package main import ( "encoding/json" "io" "log/slog" "math/rand" "net/http" "os" "path/filepath" "strconv" "strings" "text/template" "time" "git.andreafazzi.eu/andrea/probo/lib/models" "git.andreafazzi.eu/andrea/probo/lib/store" "git.andreafazzi.eu/andrea/probo/lib/store/file" "github.com/lmittmann/tint" ) var ( DefaultAssetDir = "assets" DefaultDataDir = "data" DefaultSessionDir = "sessions" DefaultResponseDir = "responses" DefaultTemplateDir = "templates" DefaultStaticDir = "static" ) type Config struct { SessionDir string ResponseDir string TemplateDir string StaticDir string } type ExamTemplateData struct { *models.Exam SessionID string } type Server struct { config *Config mux *http.ServeMux sessionFileStore *file.SessionFileStore responseFileStore *file.ResponseFileStore } func GetDefaultTemplateDir() string { return filepath.Join(DefaultAssetDir, DefaultTemplateDir) } func GetDefaultStaticDir() string { return filepath.Join(DefaultAssetDir, DefaultStaticDir) } func GetDefaultSessionDir() string { return filepath.Join(DefaultDataDir, DefaultSessionDir) } func GetDefaultResponseDir() string { return filepath.Join(DefaultDataDir, DefaultResponseDir) } func NewServer(config *Config) (*Server, error) { _, err := os.Stat(config.SessionDir) if err != nil { return nil, err } _, err = os.Stat(config.TemplateDir) if err != nil { return nil, err } _, err = os.Stat(config.StaticDir) if err != nil { return nil, err } sStore, err := file.NewSessionFileStore( &file.FileStoreConfig[*models.Session, *store.SessionStore]{ FilePathConfig: file.FilePathConfig{Dir: config.SessionDir, FilePrefix: "session", FileSuffix: ".json"}, IndexDirFunc: file.DefaultIndexDirFunc[*models.Session, *store.SessionStore], CreateEntityFunc: func() *models.Session { return &models.Session{} }, }, ) if err != nil { return nil, err } rStore, err := file.NewResponseFileStore( &file.FileStoreConfig[*models.Response, *store.ResponseStore]{ FilePathConfig: file.FilePathConfig{Dir: config.ResponseDir, FilePrefix: "response", FileSuffix: ".json"}, IndexDirFunc: file.DefaultIndexDirFunc[*models.Response, *store.ResponseStore], CreateEntityFunc: func() *models.Response { return &models.Response{} }, }, ) if err != nil { return nil, err } rStore.FilePathConfig = file.FilePathConfig{ Dir: config.ResponseDir, FilePrefix: "response", FileSuffix: ".json", } s := &Server{ config, http.NewServeMux(), sStore, rStore, } s.mux.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir(config.StaticDir)))) s.mux.HandleFunc("/create", s.createExamSessionHandler) s.mux.HandleFunc("/responses/", s.getResponsesHandler) s.mux.HandleFunc("/", s.getExamHandler) return s, nil } func NewDefaultServer() (*Server, error) { return NewServer(&Config{ SessionDir: GetDefaultSessionDir(), ResponseDir: GetDefaultResponseDir(), TemplateDir: GetDefaultTemplateDir(), StaticDir: GetDefaultStaticDir(), }) } func (s *Server) getResponsesHandler(w http.ResponseWriter, r *http.Request) { result := make([]*models.Response, 0) urlParts := strings.Split(r.URL.Path, "/") sessionID := urlParts[2] if r.Method == "GET" { session, err := s.sessionFileStore.Read(sessionID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } for _, exam := range session.Exams { responses := s.responseFileStore.ReadAll() for _, r := range responses { if r.ID == exam.ID { result = append(result, r) } } } err = json.NewEncoder(w).Encode(result) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } } func (s *Server) createExamSessionHandler(w http.ResponseWriter, r *http.Request) { session := new(models.Session) data, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } err = session.Unmarshal(data) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } memorySession, err := s.sessionFileStore.Create(session) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } err = json.NewEncoder(w).Encode(memorySession) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } slog.Info("Received a new session", "session", memorySession) } func (s *Server) getExamHandler(w http.ResponseWriter, r *http.Request) { urlParts := strings.Split(r.URL.Path, "/") sessionID := urlParts[1] token := urlParts[2] session, err := s.sessionFileStore.Read(sessionID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } exam := session.Exams[token] if r.Method == "GET" { w.Header().Set("Content-Type", "text/html") tplData, err := os.ReadFile(filepath.Join(GetDefaultTemplateDir(), "exam.tpl")) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } tmpl := template.Must(template.New("exam").Parse(string(tplData))) err = tmpl.Execute(w, ExamTemplateData{exam, session.ID}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } if r.Method == "POST" { err := r.ParseForm() if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } response := new(models.Response) response.UniqueIDFunc = func() string { return exam.GetID() } response.Questions = make(map[string]string) for qID, values := range r.Form { for _, aID := range values { response.Questions[qID] = aID } } _, err = s.responseFileStore.Create(response) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.Write([]byte("

Thank you for your response.

")) return } } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) } func generateRandomID() string { id := "" for i := 0; i < 6; i++ { id += strconv.Itoa(rand.Intn(9) + 1) } return id } func main() { slog.SetDefault(slog.New( tint.NewHandler(os.Stdout, &tint.Options{ Level: slog.LevelInfo, TimeFormat: time.Kitchen, }), )) server, err := NewDefaultServer() if err != nil { panic(err) } slog.Info("Probo server started.") http.ListenAndServe(":8080", server) }