From 84f83507003493e2f3ba5ca44de2fc555fc6205c Mon Sep 17 00:00:00 2001 From: James Date: Thu, 12 Mar 2026 03:45:39 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20server-side=20test=20role=20=E2=80=94?= =?UTF-8?q?=20PUT=20/api/admin/test-role=20writes=20to=20session,=20middle?= =?UTF-8?q?ware=20injects=20into=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/handlers.go | 32 ++++++++++++++++++++++++++++++ api/middleware.go | 18 ++++++++++++++++- api/routes.go | 1 + data/dealspace.db-shm | Bin 32768 -> 32768 bytes data/dealspace.db-wal | Bin 57712 -> 65952 bytes lib/dbcore.go | 6 ++++++ lib/types.go | 1 + portal/templates/layouts/app.html | 23 +++++++++++++-------- 8 files changed, 72 insertions(+), 9 deletions(-) diff --git a/api/handlers.go b/api/handlers.go index 665f4e0..3754749 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -2852,3 +2852,35 @@ func containsAny(s string, subs ...string) bool { } // UpdateOrg handles PUT /api/orgs/{orgId} — update org name, domains, role, website + +// SetTestRole handles PUT /api/admin/test-role — super admin sets their test role for the current session. +func (h *Handlers) SetTestRole(w http.ResponseWriter, r *http.Request) { + actorID := UserIDFromContext(r.Context()) + isSuperAdmin, _ := lib.IsSuperAdmin(h.DB, actorID) + if !isSuperAdmin { + ErrorResponse(w, http.StatusForbidden, "access_denied", "Super admin only") + return + } + sessionID := SessionIDFromContext(r.Context()) + if sessionID == "" { + ErrorResponse(w, http.StatusBadRequest, "no_session", "No session found") + return + } + var req struct { + Role string `json:"role"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + ErrorResponse(w, http.StatusBadRequest, "invalid_json", "Invalid request body") + return + } + validRoles := map[string]bool{"": true, "ib": true, "buyer": true, "seller": true, "advisor": true} + if !validRoles[req.Role] { + ErrorResponse(w, http.StatusBadRequest, "invalid_role", "Role must be one of: ib, buyer, seller, advisor (or empty to reset)") + return + } + if err := lib.SetSessionTestRole(h.DB, sessionID, req.Role); err != nil { + ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to set test role") + return + } + JSONResponse(w, http.StatusOK, map[string]any{"ok": true, "test_role": req.Role}) +} diff --git a/api/middleware.go b/api/middleware.go index 84a024c..75a704d 100644 --- a/api/middleware.go +++ b/api/middleware.go @@ -18,7 +18,9 @@ import ( type contextKey string const ( - ctxUserID contextKey = "user_id" + ctxUserID contextKey = "user_id" + ctxTestRole contextKey = "test_role" + ctxSessID contextKey = "session_id" ) // UserIDFromContext extracts the authenticated user ID from the request context. @@ -27,6 +29,18 @@ func UserIDFromContext(ctx context.Context) string { return v } +// TestRoleFromContext returns the active test role (empty = none). +func TestRoleFromContext(ctx context.Context) string { + v, _ := ctx.Value(ctxTestRole).(string) + return v +} + +// SessionIDFromContext returns the session ID from context. +func SessionIDFromContext(ctx context.Context) string { + v, _ := ctx.Value(ctxSessID).(string) + return v +} + // AuthMiddleware validates JWT tokens and sets user context. func AuthMiddleware(db *lib.DB, jwtSecret []byte) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { @@ -55,6 +69,8 @@ func AuthMiddleware(db *lib.DB, jwtSecret []byte) func(http.Handler) http.Handle } ctx := context.WithValue(r.Context(), ctxUserID, claims.UserID) + ctx = context.WithValue(ctx, ctxTestRole, session.TestRole) + ctx = context.WithValue(ctx, ctxSessID, claims.SessionID) next.ServeHTTP(w, r.WithContext(ctx)) }) } diff --git a/api/routes.go b/api/routes.go index 56a2fcf..1e05c51 100644 --- a/api/routes.go +++ b/api/routes.go @@ -99,6 +99,7 @@ func NewRouter(db *lib.DB, cfg *lib.Config, store lib.ObjectStore, websiteFS fs. r.Get("/orgs", h.ListOrgs) r.Post("/orgs", h.CreateOrg) r.Put("/orgs/{orgID}", h.UpdateOrg) + r.Put("/admin/test-role", h.SetTestRole) r.Get("/orgs/{orgID}", h.GetOrg) r.Patch("/orgs/{orgID}", h.UpdateOrg) diff --git a/data/dealspace.db-shm b/data/dealspace.db-shm index 4a0d3890d2c74dac660c1695b10d0a3e31205516..e393e42fcff10b335328fee09b4b506763c57489 100644 GIT binary patch delta 142 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 delta 9 QcmZ3`%<|z7^M-b.classList.toggle('active',b.getAttribute('data-t')===t))}