package main import ( "context" _ "embed" "encoding/json" "fmt" "net/http" "os" "os/signal" "path" "strings" "syscall" "time" ) type Context struct { DB DB User IDU } func HTTP(port int, db DB) error { ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT) defer can() foo := func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { httpGUI(w, r) } else if strings.HasPrefix(r.URL.Path, "/api/questions") && strings.HasSuffix(r.URL.Path, "/answers") && r.Method == http.MethodPost { httpPostQuestionAnswers(w, r) } else { http.NotFound(w, r) } } foo = withAuth(foo) foo = withDB(foo, db) foo = withCtx(foo, ctx) go func() { http.ListenAndServe(fmt.Sprintf(":%d", port), http.HandlerFunc(foo)) }() <-ctx.Done() return nil } func extract(ctx context.Context) Context { v := ctx.Value("__context") v2, _ := v.(Context) return v2 } func inject(ctx context.Context, v Context) context.Context { return context.WithValue(ctx, "__context", v) } func withCtx(foo http.HandlerFunc, ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { r = r.WithContext(ctx) foo(w, r) } } func withDB(foo http.HandlerFunc, db DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { c := extract(r.Context()) c.DB = db r = r.WithContext(inject(r.Context(), c)) foo(w, r) } } func withAuth(foo http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { u, _, ok := r.BasicAuth() if !ok || u == "" { w.Header().Set("WWW-Authenticate", "Basic") w.WriteHeader(401) return } c := extract(r.Context()) c.User = IDU(u) r = r.WithContext(inject(r.Context(), c)) foo(w, r) } } //go:embed public/root.html var httpGUIHTML string func httpGUI(w http.ResponseWriter, r *http.Request) { body := httpGUIHTML if os.Getenv("DEBUG") != "" { b, _ := os.ReadFile("public/root.html") body = string(b) } ctx := extract(r.Context()) body = strings.ReplaceAll(body, "{{USER}}", string(ctx.User)) assignments, err := httpAssignments(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } assignmentsB, _ := json.Marshal(assignments) body = strings.ReplaceAll(body, "{{ASSIGNMENTS_JSON}}", string(assignmentsB)) w.Write([]byte(body)) } func httpAssignments(ctx context.Context) (interface{}, error) { db := extract(ctx).DB user := extract(ctx).User todo := map[IDQ]Question{} for q, _ := range db.HistoryOf(user) { if time.Until(db.Next(user, q)) > 0 { continue } todo[q] = db.Question(q) } return todo, nil } func httpPostQuestionAnswers(w http.ResponseWriter, r *http.Request) { idq := IDQ(path.Base(strings.Split(r.URL.Path, "/answers")[0])) var payload struct { Answer string `json:"answer"` Passed bool `json:"passed"` } if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } ctx := extract(r.Context()) if err := ctx.DB.PushAnswer(ctx.User, idq, Renderable(payload.Answer), payload.Passed); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }