main.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. package main
  2. import (
  3. "encoding/json"
  4. "io"
  5. "log/slog"
  6. "math/rand"
  7. "net/http"
  8. "os"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. "text/template"
  13. "time"
  14. "git.andreafazzi.eu/andrea/probo/pkg/models"
  15. "git.andreafazzi.eu/andrea/probo/pkg/store"
  16. "git.andreafazzi.eu/andrea/probo/pkg/store/file"
  17. "github.com/lmittmann/tint"
  18. )
  19. var (
  20. DefaultAssetDir = "assets"
  21. DefaultDataDir = "data"
  22. DefaultSessionDir = "sessions"
  23. DefaultResponseDir = "responses"
  24. DefaultTemplateDir = "templates"
  25. DefaultStaticDir = "static"
  26. )
  27. type Config struct {
  28. SessionDir string
  29. ResponseDir string
  30. TemplateDir string
  31. StaticDir string
  32. }
  33. type ExamTemplateData struct {
  34. *models.Exam
  35. SessionID string
  36. }
  37. type Server struct {
  38. config *Config
  39. mux *http.ServeMux
  40. sessionFileStore *file.SessionFileStore
  41. responseFileStore *file.ResponseFileStore
  42. }
  43. func GetDefaultTemplateDir() string {
  44. return filepath.Join(DefaultAssetDir, DefaultTemplateDir)
  45. }
  46. func GetDefaultStaticDir() string {
  47. return filepath.Join(DefaultAssetDir, DefaultStaticDir)
  48. }
  49. func GetDefaultSessionDir() string {
  50. return filepath.Join(DefaultDataDir, DefaultSessionDir)
  51. }
  52. func GetDefaultResponseDir() string {
  53. return filepath.Join(DefaultDataDir, DefaultResponseDir)
  54. }
  55. func NewServer(config *Config) (*Server, error) {
  56. _, err := os.Stat(config.SessionDir)
  57. if err != nil {
  58. return nil, err
  59. }
  60. _, err = os.Stat(config.TemplateDir)
  61. if err != nil {
  62. return nil, err
  63. }
  64. _, err = os.Stat(config.StaticDir)
  65. if err != nil {
  66. return nil, err
  67. }
  68. sStore, err := file.NewSessionFileStore(
  69. &file.FileStoreConfig[*models.Session, *store.SessionStore]{
  70. FilePathConfig: file.FilePathConfig{Dir: config.SessionDir, FilePrefix: "session", FileSuffix: ".json"},
  71. IndexDirFunc: file.DefaultIndexDirFunc[*models.Session, *store.SessionStore],
  72. CreateEntityFunc: func() *models.Session {
  73. return &models.Session{}
  74. },
  75. },
  76. )
  77. if err != nil {
  78. return nil, err
  79. }
  80. rStore, err := file.NewResponseFileStore(
  81. &file.FileStoreConfig[*models.Response, *store.ResponseStore]{
  82. FilePathConfig: file.FilePathConfig{Dir: config.ResponseDir, FilePrefix: "response", FileSuffix: ".json"},
  83. IndexDirFunc: file.DefaultIndexDirFunc[*models.Response, *store.ResponseStore],
  84. CreateEntityFunc: func() *models.Response {
  85. return &models.Response{}
  86. },
  87. },
  88. )
  89. if err != nil {
  90. return nil, err
  91. }
  92. rStore.FilePathConfig = file.FilePathConfig{
  93. Dir: config.ResponseDir,
  94. FilePrefix: "response",
  95. FileSuffix: ".json",
  96. }
  97. s := &Server{
  98. config,
  99. http.NewServeMux(),
  100. sStore,
  101. rStore,
  102. }
  103. s.mux.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir(config.StaticDir))))
  104. s.mux.HandleFunc("/create", s.createExamSessionHandler)
  105. s.mux.HandleFunc("/responses/", s.getResponsesHandler)
  106. s.mux.HandleFunc("/", s.getExamHandler)
  107. return s, nil
  108. }
  109. func NewDefaultServer() (*Server, error) {
  110. return NewServer(&Config{
  111. SessionDir: GetDefaultSessionDir(),
  112. ResponseDir: GetDefaultResponseDir(),
  113. TemplateDir: GetDefaultTemplateDir(),
  114. StaticDir: GetDefaultStaticDir(),
  115. })
  116. }
  117. func (s *Server) getResponsesHandler(w http.ResponseWriter, r *http.Request) {
  118. result := make([]*models.Response, 0)
  119. urlParts := strings.Split(r.URL.Path, "/")
  120. sessionID := urlParts[2]
  121. if r.Method == "GET" {
  122. session, err := s.sessionFileStore.Read(sessionID)
  123. if err != nil {
  124. http.Error(w, err.Error(), http.StatusInternalServerError)
  125. return
  126. }
  127. for _, exam := range session.Exams {
  128. responses := s.responseFileStore.ReadAll()
  129. for _, r := range responses {
  130. if r.ID == exam.ID {
  131. result = append(result, r)
  132. }
  133. }
  134. }
  135. err = json.NewEncoder(w).Encode(result)
  136. if err != nil {
  137. http.Error(w, err.Error(), http.StatusInternalServerError)
  138. return
  139. }
  140. }
  141. }
  142. func (s *Server) createExamSessionHandler(w http.ResponseWriter, r *http.Request) {
  143. session := new(models.Session)
  144. data, err := io.ReadAll(r.Body)
  145. if err != nil {
  146. http.Error(w, err.Error(), http.StatusBadRequest)
  147. return
  148. }
  149. err = session.Unmarshal(data)
  150. if err != nil {
  151. http.Error(w, err.Error(), http.StatusBadRequest)
  152. return
  153. }
  154. memorySession, err := s.sessionFileStore.Create(session)
  155. if err != nil {
  156. http.Error(w, err.Error(), http.StatusInternalServerError)
  157. return
  158. }
  159. err = json.NewEncoder(w).Encode(memorySession)
  160. if err != nil {
  161. http.Error(w, err.Error(), http.StatusInternalServerError)
  162. return
  163. }
  164. slog.Info("Received a new session", "session", memorySession)
  165. }
  166. func (s *Server) getExamHandler(w http.ResponseWriter, r *http.Request) {
  167. urlParts := strings.Split(r.URL.Path, "/")
  168. sessionID := urlParts[1]
  169. token := urlParts[2]
  170. session, err := s.sessionFileStore.Read(sessionID)
  171. if err != nil {
  172. http.Error(w, err.Error(), http.StatusInternalServerError)
  173. return
  174. }
  175. exam := session.Exams[token]
  176. if r.Method == "GET" {
  177. w.Header().Set("Content-Type", "text/html")
  178. tplData, err := os.ReadFile(filepath.Join(GetDefaultTemplateDir(), "exam.tpl"))
  179. if err != nil {
  180. http.Error(w, err.Error(), http.StatusInternalServerError)
  181. return
  182. }
  183. tmpl := template.Must(template.New("exam").Parse(string(tplData)))
  184. err = tmpl.Execute(w, ExamTemplateData{exam, session.ID})
  185. if err != nil {
  186. http.Error(w, err.Error(), http.StatusInternalServerError)
  187. return
  188. }
  189. }
  190. if r.Method == "POST" {
  191. err := r.ParseForm()
  192. if err != nil {
  193. http.Error(w, err.Error(), http.StatusBadRequest)
  194. return
  195. }
  196. response := new(models.Response)
  197. response.UniqueIDFunc = func() string {
  198. return exam.GetID()
  199. }
  200. response.Questions = make(map[string]string)
  201. for qID, values := range r.Form {
  202. for _, aID := range values {
  203. response.Questions[qID] = aID
  204. }
  205. }
  206. _, err = s.responseFileStore.Create(response)
  207. if err != nil {
  208. http.Error(w, err.Error(), http.StatusBadRequest)
  209. return
  210. }
  211. w.Write([]byte("<p>Thank you for your response.</p>"))
  212. return
  213. }
  214. }
  215. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  216. s.mux.ServeHTTP(w, r)
  217. }
  218. func generateRandomID() string {
  219. id := ""
  220. for i := 0; i < 6; i++ {
  221. id += strconv.Itoa(rand.Intn(9) + 1)
  222. }
  223. return id
  224. }
  225. func main() {
  226. slog.SetDefault(slog.New(
  227. tint.NewHandler(os.Stdout, &tint.Options{
  228. Level: slog.LevelInfo,
  229. TimeFormat: time.Kitchen,
  230. }),
  231. ))
  232. server, err := NewDefaultServer()
  233. if err != nil {
  234. panic(err)
  235. }
  236. slog.Info("Probo server started.")
  237. http.ListenAndServe(":8080", server)
  238. }