feat(httpapi): websocket, router, embed static, main entrypoint

This commit is contained in:
2026-07-01 18:37:48 +07:00
parent 0bd4ba37e3
commit 9ec6acd414
8 changed files with 269 additions and 16 deletions
+30
View File
@@ -0,0 +1,30 @@
package httpapi
import "net/http"
func (s *Server) Router() http.Handler {
mux := http.NewServeMux()
// открытые
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
mux.HandleFunc("POST /api/login", s.handleLogin)
mux.HandleFunc("POST /api/logout", s.handleLogout)
// защищённые
api := http.NewServeMux()
api.HandleFunc("GET /api/endpoints", s.handleListEndpoints)
api.HandleFunc("POST /api/endpoints", s.handleCreateEndpoint)
api.HandleFunc("GET /api/tasks", s.handleListTasks)
api.HandleFunc("POST /api/tasks", s.handleCreateTask)
api.HandleFunc("GET /api/tasks/{id}", s.handleGetTask)
api.HandleFunc("POST /api/tasks/{id}/accounts", s.handleCreateAccount)
api.HandleFunc("POST /api/tasks/{id}/import", s.handleImportCSV)
api.HandleFunc("POST /api/tasks/{id}/test", s.handleTestAccounts)
api.HandleFunc("POST /api/tasks/{id}/run", s.handleRun)
mux.Handle("/api/", s.requireAuth(api))
mux.Handle("/ws", s.requireAuth(http.HandlerFunc(s.handleWS)))
// SPA static (fallback)
mux.Handle("/", s.staticHandler())
return mux
}
+27
View File
@@ -0,0 +1,27 @@
package httpapi
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/vasyansk/imap-copier/internal/config"
)
func TestHealthzOpen(t *testing.T) {
s := &Server{cfg: config.Config{SessionSecret: []byte("x")}}
rw := httptest.NewRecorder()
s.Router().ServeHTTP(rw, httptest.NewRequest("GET", "/healthz", nil))
if rw.Code != http.StatusOK {
t.Fatalf("healthz=%d", rw.Code)
}
}
func TestTasksRequiresAuth(t *testing.T) {
s := &Server{cfg: config.Config{SessionSecret: []byte("x")}}
rw := httptest.NewRecorder()
s.Router().ServeHTTP(rw, httptest.NewRequest("GET", "/api/tasks", nil))
if rw.Code != http.StatusUnauthorized {
t.Fatalf("want 401, got %d", rw.Code)
}
}
+35
View File
@@ -0,0 +1,35 @@
package httpapi
import (
"embed"
"io/fs"
"net/http"
)
//go:embed all:webdist
var webDist embed.FS
func (s *Server) staticHandler() http.Handler {
sub, err := fs.Sub(webDist, "webdist")
if err != nil {
panic(err)
}
fileServer := http.FileServer(http.FS(sub))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// SPA fallback: если файла нет — отдать index.html
if _, err := fs.Stat(sub, trimLead(r.URL.Path)); err != nil && r.URL.Path != "/" {
r2 := r.Clone(r.Context())
r2.URL.Path = "/"
fileServer.ServeHTTP(w, r2)
return
}
fileServer.ServeHTTP(w, r)
})
}
func trimLead(p string) string {
if len(p) > 0 && p[0] == '/' {
return p[1:]
}
return p
}
+1
View File
@@ -0,0 +1 @@
<!doctype html><title>imap-copier</title>
+45
View File
@@ -0,0 +1,45 @@
package httpapi
import (
"context"
"net/http"
"strconv"
"time"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
)
func (s *Server) handleWS(w http.ResponseWriter, r *http.Request) {
taskID, err := strconv.ParseInt(r.URL.Query().Get("task_id"), 10, 64)
if err != nil {
http.Error(w, "task_id required", http.StatusBadRequest)
return
}
c, err := websocket.Accept(w, r, nil)
if err != nil {
return
}
defer c.CloseNow()
subID, ch := s.hub.Subscribe(taskID)
defer s.hub.Unsubscribe(taskID, subID)
ctx := r.Context()
for {
select {
case ev, ok := <-ch:
if !ok {
return
}
wctx, cancel := context.WithTimeout(ctx, 5*time.Second)
err := wsjson.Write(wctx, c, ev)
cancel()
if err != nil {
return
}
case <-ctx.Done():
return
}
}
}