This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/profile_bootstrap.go
Jeffrey Smith b10f5bee05
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m22s
CI/CD / test-sqlite (pull_request) Successful in 2m35s
CI/CD / build-and-deploy (pull_request) Successful in 1m18s
drop users.role column: full RBAC through group membership
The role column was a pre-RBAC artifact. All authorization now flows
through explicit group membership and permission grants:

- Everyone group: all users added on creation (no implicit membership)
- Admins group: grants surface.admin.access + all platform permissions
- JWT claims, login response, profile: role field removed
- OIDC: isIdPAdmin() maps IdP claims → Admins group (no role writes)
- Admin UI: role dropdown removed, admin managed through groups
- Middleware cache simplified to isActive only

28 files changed, -79 lines net. Zero magic roles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:23:12 +00:00

109 lines
3.3 KiB
Go

package handlers
// profile_bootstrap.go — Single-call boot payload for the SDK.
//
// GET /api/v1/profile/bootstrap
//
// Collapses what previously required 3-4 sequential requests
// (profile, permissions, teams/mine, settings) into one call.
// The SDK calls this at startup and on token refresh.
//
// v0.37.15
import (
"net/http"
"sort"
"github.com/gin-gonic/gin"
"switchboard-core/auth"
"switchboard-core/store"
)
// ProfileBootstrapHandler serves the combined boot payload.
type ProfileBootstrapHandler struct {
stores store.Stores
}
func NewProfileBootstrapHandler(s store.Stores) *ProfileBootstrapHandler {
return &ProfileBootstrapHandler{stores: s}
}
// GetBootstrap returns everything the shell needs at startup.
// GET /api/v1/profile/bootstrap
func (h *ProfileBootstrapHandler) GetBootstrap(c *gin.Context) {
userID := getUserID(c)
ctx := c.Request.Context()
// ── User profile ────────────────────────
user, err := h.stores.Users.GetByID(ctx, userID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
userPayload := gin.H{
"id": user.ID,
"username": user.Username,
"display_name": user.DisplayName,
"email": user.Email,
}
if user.AvatarURL != "" {
userPayload["avatar"] = user.AvatarURL
}
// ── Permissions ─────────────────────────
perms, err := auth.ResolvePermissions(ctx, h.stores, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to resolve permissions"})
return
}
permList := make([]string, 0, len(perms))
for p := range perms {
permList = append(permList, p)
}
sort.Strings(permList)
// ── Groups ──────────────────────────────
groupIDs, _ := h.stores.Groups.GetUserGroupIDs(ctx, userID)
if groupIDs == nil {
groupIDs = []string{}
}
// ── Teams ───────────────────────────────
teams, _ := h.stores.Teams.ListForUser(ctx, userID)
teamData := make([]gin.H, 0, len(teams))
for _, t := range teams {
teamData = append(teamData, gin.H{
"id": t.ID,
"name": t.Name,
"my_role": t.MyRole,
})
}
// ── Policies ────────────────────────────
policies := make(map[string]bool)
if ps := h.stores.Policies; ps != nil {
policies["allow_user_byok"], _ = ps.GetBool(ctx, "allow_user_byok")
policies["allow_user_personas"], _ = ps.GetBool(ctx, "allow_user_personas")
policies["allow_raw_model_access"], _ = ps.GetBool(ctx, "allow_raw_model_access")
policies["kb_direct_access"], _ = ps.GetBool(ctx, "kb_direct_access")
}
// ── Settings ────────────────────────────
settings := make(map[string]interface{})
if user.Settings != nil {
settings = user.Settings
}
// ── Response ────────────────────────────
c.JSON(http.StatusOK, gin.H{
"user": userPayload,
"permissions": permList,
"groups": groupIDs,
"teams": teamData,
"policies": policies,
"settings": settings,
})
}