package httpapi import ( "encoding/json" "errors" "net/http" "strings" "time" "momswap/backend/internal/app" "momswap/backend/internal/store" ) type API struct { service *app.Service } func NewAPI(svc *app.Service) *API { return &API{service: svc} } func (a *API) Routes() http.Handler { mux := http.NewServeMux() staticFiles := http.FileServer(http.Dir("web")) libsFiles := http.FileServer(http.Dir("libs")) mux.HandleFunc("GET /healthz", a.health) mux.HandleFunc("GET /v1/service-key", a.getServiceKey) mux.HandleFunc("POST /v1/auth/challenge", a.createChallenge) mux.HandleFunc("POST /v1/auth/login", a.login) mux.HandleFunc("POST /v1/auth/register", a.register) mux.HandleFunc("POST /v1/auth/register-by-signature", a.registerBySignature) mux.HandleFunc("POST /v1/invitations", a.createInvitation) mux.HandleFunc("GET /v1/me/keys", a.me) mux.HandleFunc("POST /v1/collections", a.createCollection) mux.HandleFunc("GET /v1/collections", a.listCollections) mux.HandleFunc("PATCH /v1/collections/{id}", a.updateCollection) mux.HandleFunc("DELETE /v1/collections/{id}", a.deleteCollection) mux.HandleFunc("POST /v1/collections/{id}/features", a.createFeature) mux.HandleFunc("GET /v1/collections/{id}/features", a.listFeatures) mux.HandleFunc("DELETE /v1/features/{id}", a.deleteFeature) mux.HandleFunc("POST /v1/assets", a.createAsset) mux.HandleFunc("PATCH /v1/assets/{id}", a.patchAsset) mux.HandleFunc("POST /v1/assets/{id}/signed-upload", a.signedUpload) mux.HandleFunc("GET /v1/assets/{id}/download", a.downloadAsset) mux.Handle("/web/", http.StripPrefix("/web/", staticFiles)) mux.Handle("/libs/", http.StripPrefix("/libs/", libsFiles)) mux.Handle("/", http.RedirectHandler("/web/", http.StatusTemporaryRedirect)) return withCORS(mux) } func withCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Vary", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD") w.Header().Set("Access-Control-Max-Age", "86400") requestHeaders := r.Header.Get("Access-Control-Request-Headers") if requestHeaders != "" { w.Header().Set("Access-Control-Allow-Headers", requestHeaders) } else { w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, Origin, X-Requested-With") } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func writeJSON(w http.ResponseWriter, status int, payload interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(payload) } func readJSON(r *http.Request, target interface{}) error { dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() return dec.Decode(target) } func statusFromErr(err error) int { switch { case errors.Is(err, app.ErrUnauthorized): return http.StatusUnauthorized case errors.Is(err, app.ErrForbidden): return http.StatusForbidden case errors.Is(err, app.ErrBadRequest): return http.StatusBadRequest case errors.Is(err, app.ErrAlreadyUser): return http.StatusConflict case errors.Is(err, app.ErrRegistrationNotConfigured): return http.StatusServiceUnavailable case errors.Is(err, app.ErrInviteInvalid), errors.Is(err, app.ErrInviteExpired), errors.Is(err, app.ErrInviteExhaust): return http.StatusBadRequest case errors.Is(err, app.ErrCollectionMiss), errors.Is(err, app.ErrFeatureMiss), errors.Is(err, app.ErrAssetMiss), errors.Is(err, store.ErrNotFound): return http.StatusNotFound case errors.Is(err, app.ErrStorageNotConfigured): return http.StatusServiceUnavailable default: return http.StatusInternalServerError } } func writeErr(w http.ResponseWriter, err error) { payload := map[string]string{"error": err.Error()} if errors.Is(err, app.ErrRegistrationNotConfigured) { payload["code"] = "REGISTRATION_NOT_CONFIGURED" payload["hint"] = "Run ./bin/gen-server-keys.sh to create etc/server-service.env, or set ADMIN_PUBLIC_KEY before starting the API." } writeJSON(w, statusFromErr(err), payload) } func bearerToken(r *http.Request) (string, error) { h := r.Header.Get("Authorization") if h == "" { return "", app.ErrUnauthorized } parts := strings.SplitN(h, " ", 2) if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || parts[1] == "" { return "", app.ErrUnauthorized } return parts[1], nil } func (a *API) authUser(r *http.Request) (string, error) { token, err := bearerToken(r) if err != nil { return "", err } return a.service.AuthenticateSession(token) } func (a *API) health(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "time": time.Now().UTC().Format(time.RFC3339)}) } func (a *API) createChallenge(w http.ResponseWriter, r *http.Request) { var req struct { PublicKey string `json:"publicKey"` } if err := readJSON(r, &req); err != nil { writeErr(w, app.ErrBadRequest) return } nonce, err := a.service.CreateChallenge(req.PublicKey, clientIP(r)) if err != nil { writeErr(w, err) return } writeJSON(w, http.StatusOK, map[string]string{"nonce": nonce, "messageToSign": "login:" + nonce}) } func (a *API) login(w http.ResponseWriter, r *http.Request) { var req struct { PublicKey string `json:"publicKey"` Nonce string `json:"nonce"` Signature string `json:"signature"` } if err := readJSON(r, &req); err != nil { writeErr(w, app.ErrBadRequest) return } token, err := a.service.Login(req.PublicKey, req.Nonce, req.Signature, clientIP(r)) if err != nil { writeErr(w, err) return } writeJSON(w, http.StatusOK, map[string]string{"accessToken": token}) } func (a *API) getServiceKey(w http.ResponseWriter, _ *http.Request) { pk := a.service.ServicePublicKey() if pk == "" { writeErr(w, app.ErrRegistrationNotConfigured) return } writeJSON(w, http.StatusOK, map[string]string{"publicKey": pk}) } func (a *API) register(w http.ResponseWriter, r *http.Request) { var req struct { PublicKey string `json:"publicKey"` InvitePayloadB64 string `json:"invitePayloadB64"` InviteSignature string `json:"inviteSignature"` ProofSignature string `json:"proofSignature"` } if err := readJSON(r, &req); err != nil { writeErr(w, app.ErrBadRequest) return } if err := a.service.Register(req.PublicKey, req.InvitePayloadB64, req.InviteSignature, req.ProofSignature); err != nil { writeErr(w, err) return } writeJSON(w, http.StatusCreated, map[string]string{"status": "registered"}) } func (a *API) registerBySignature(w http.ResponseWriter, r *http.Request) { var req struct { PublicKey string `json:"publicKey"` Signature string `json:"signature"` } if err := readJSON(r, &req); err != nil { writeErr(w, app.ErrBadRequest) return } if err := a.service.RegisterBySignature(req.PublicKey, req.Signature); err != nil { writeErr(w, err) return } writeJSON(w, http.StatusCreated, map[string]string{"status": "registered"}) } func (a *API) createInvitation(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } var req struct { InvitePayloadB64 string `json:"invitePayloadB64"` InviteSignature string `json:"inviteSignature"` } if err := readJSON(r, &req); err != nil { writeErr(w, app.ErrBadRequest) return } if err := a.service.CreateInvitation(user, req.InvitePayloadB64, req.InviteSignature); err != nil { writeErr(w, err) return } writeJSON(w, http.StatusCreated, map[string]string{"status": "invitation stored"}) } func (a *API) me(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } writeJSON(w, http.StatusOK, map[string]string{"publicKey": user}) } func (a *API) createCollection(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } var req struct { Name string `json:"name"` } if err := readJSON(r, &req); err != nil { writeErr(w, app.ErrBadRequest) return } collection, err := a.service.CreateCollection(user, req.Name) if err != nil { writeErr(w, err) return } writeJSON(w, http.StatusCreated, collection) } func (a *API) listCollections(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } writeJSON(w, http.StatusOK, map[string]interface{}{"collections": a.service.ListCollections(user)}) } func (a *API) updateCollection(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } collectionID := r.PathValue("id") var req struct { Name string `json:"name"` } if err := readJSON(r, &req); err != nil { writeErr(w, app.ErrBadRequest) return } collection, err := a.service.UpdateCollection(user, collectionID, req.Name) if err != nil { writeErr(w, err) return } writeJSON(w, http.StatusOK, collection) } func (a *API) deleteCollection(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } collectionID := r.PathValue("id") if err := a.service.DeleteCollection(user, collectionID); err != nil { writeErr(w, err) return } w.WriteHeader(http.StatusNoContent) } func (a *API) createFeature(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } collectionID := r.PathValue("id") var req struct { Geometry store.Point `json:"geometry"` Properties map[string]interface{} `json:"properties"` } if err := readJSON(r, &req); err != nil { writeErr(w, app.ErrBadRequest) return } feature, err := a.service.CreateFeature(user, collectionID, req.Geometry, req.Properties) if err != nil { writeErr(w, err) return } writeJSON(w, http.StatusCreated, feature) } func (a *API) listFeatures(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } collectionID := r.PathValue("id") features, err := a.service.ListFeatures(user, collectionID) if err != nil { writeErr(w, err) return } writeJSON(w, http.StatusOK, map[string]interface{}{"features": features}) } func (a *API) deleteFeature(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } featureID := r.PathValue("id") if err := a.service.DeleteFeature(user, featureID); err != nil { writeErr(w, err) return } w.WriteHeader(http.StatusNoContent) } func (a *API) createAsset(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } var req struct { FeatureID string `json:"featureId"` Checksum string `json:"checksum"` Ext string `json:"ext"` Kind string `json:"kind"` MimeType string `json:"mimeType"` SizeBytes int64 `json:"sizeBytes"` Name string `json:"name"` Description string `json:"description"` IsPublic *bool `json:"isPublic"` } if err := readJSON(r, &req); err != nil { writeErr(w, app.ErrBadRequest) return } asset, created, err := a.service.CreateOrLinkAsset(user, app.CreateAssetInput{ FeatureID: req.FeatureID, Checksum: req.Checksum, Ext: req.Ext, Kind: req.Kind, MimeType: req.MimeType, SizeBytes: req.SizeBytes, Name: req.Name, Description: req.Description, Visibility: req.IsPublic, }) if err != nil { writeErr(w, err) return } status := http.StatusOK if created { status = http.StatusCreated } writeJSON(w, status, map[string]interface{}{ "asset": asset, "link": "/v1/assets/" + asset.ID + "/download", }) } func (a *API) patchAsset(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } assetID := r.PathValue("id") var req struct { IsPublic bool `json:"isPublic"` } if err := readJSON(r, &req); err != nil { writeErr(w, app.ErrBadRequest) return } asset, err := a.service.SetAssetPublic(user, assetID, req.IsPublic) if err != nil { writeErr(w, err) return } writeJSON(w, http.StatusOK, map[string]interface{}{"asset": asset, "link": "/v1/assets/" + asset.ID + "/download"}) } func (a *API) signedUpload(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } assetID := r.PathValue("id") var req struct { ContentType string `json:"contentType"` } if err := readJSON(r, &req); err != nil { writeErr(w, app.ErrBadRequest) return } url, err := a.service.SignedUploadURL(user, assetID, req.ContentType) if err != nil { writeErr(w, err) return } writeJSON(w, http.StatusOK, map[string]string{"url": url, "method": http.MethodPut}) } func (a *API) downloadAsset(w http.ResponseWriter, r *http.Request) { user, err := a.authUser(r) if err != nil { writeErr(w, err) return } assetID := r.PathValue("id") url, err := a.service.SignedDownloadURL(user, assetID) if err != nil { writeErr(w, err) return } http.Redirect(w, r, url, http.StatusFound) }