123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- 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)
- }
|