|
@@ -0,0 +1,290 @@
|
|
|
+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/pkg/models"
|
|
|
+ "git.andreafazzi.eu/andrea/probo/pkg/store"
|
|
|
+ "git.andreafazzi.eu/andrea/probo/pkg/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("<p>Thank you for your response.</p>"))
|
|
|
+ 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)
|
|
|
+}
|