//go:build server package main import ( "embed" "encoding/json" "errors" "io/fs" "log" "net/http" "os" "strconv" "strings" "time" ) //go:embed all:frontend/dist var serverAssets embed.FS func main() { service := NewAppService() mux := http.NewServeMux() registerAPI(mux, service) registerStatic(mux) port := strings.TrimSpace(os.Getenv("PORT")) if port == "" { port = "8080" } host := strings.TrimSpace(os.Getenv("HOST")) if host == "" { host = strings.TrimSpace(os.Getenv("WAILS_SERVER_HOST")) } if host == "" { host = "127.0.0.1" } addr := host + ":" + port server := &http.Server{ Addr: addr, Handler: logRequests(mux), ReadHeaderTimeout: 10 * time.Second, } log.Printf("OPIc Flow server listening on http://%s", addr) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) } } func registerAPI(mux *http.ServeMux, service *AppService) { mux.HandleFunc("GET /api/settings", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, service.GetSettings()) }) mux.HandleFunc("POST /api/configure", func(w http.ResponseWriter, r *http.Request) { var request ConfigureRequest if !readJSON(w, r, &request) { return } result, err := service.Configure(request) writeResult(w, result, err) }) mux.HandleFunc("POST /api/test-connection", func(w http.ResponseWriter, r *http.Request) { writeResult(w, map[string]bool{"ok": true}, service.TestConnection()) }) mux.HandleFunc("POST /api/speech", func(w http.ResponseWriter, r *http.Request) { var request struct { Text string `json:"text"` } if !readJSON(w, r, &request) { return } result, err := service.GenerateSpeech(request.Text) writeResult(w, result, err) }) mux.HandleFunc("POST /api/sessions", func(w http.ResponseWriter, r *http.Request) { var request ExamConfig if !readJSON(w, r, &request) { return } result, err := service.StartSession(request) writeResult(w, result, err) }) mux.HandleFunc("POST /api/answers", func(w http.ResponseWriter, r *http.Request) { var request SubmitAnswerRequest if !readJSON(w, r, &request) { return } result, err := service.SubmitAnswer(request) writeResult(w, result, err) }) mux.HandleFunc("POST /api/sessions/{sessionID}/finalize", func(w http.ResponseWriter, r *http.Request) { result, err := service.FinalizeSession(r.PathValue("sessionID")) writeResult(w, result, err) }) mux.HandleFunc("GET /api/sessions/{sessionID}/report", func(w http.ResponseWriter, r *http.Request) { result, err := service.GetReport(r.PathValue("sessionID")) writeResult(w, result, err) }) } func registerStatic(mux *http.ServeMux) { dist, err := fs.Sub(serverAssets, "frontend/dist") if err != nil { log.Fatalf("frontend assets are missing: %v", err) } fileServer := http.FileServer(http.FS(dist)) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { if _, err := fs.Stat(dist, strings.TrimPrefix(r.URL.Path, "/")); err != nil { r.URL.Path = "/" } } fileServer.ServeHTTP(w, r) }) } func readJSON(w http.ResponseWriter, r *http.Request, target any) bool { defer r.Body.Close() decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(target); err != nil { http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest) return false } return true } func writeResult(w http.ResponseWriter, value any, err error) { if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } writeJSON(w, value) } func writeJSON(w http.ResponseWriter, value any) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(value); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func logRequests(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %s", r.Method, r.URL.Path, strconv.FormatInt(time.Since(start).Milliseconds(), 10)+"ms") }) }