drop users.role column: full RBAC through group membership
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
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
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>
This commit is contained in:
@@ -69,29 +69,25 @@ func (h *AdminHandler) CreateUser(c *gin.Context) {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email string `json:"email" binding:"required"`
|
||||
Password string `json:"password" binding:"required,min=8"`
|
||||
Role string `json:"role"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcryptCost)
|
||||
role := models.UserRoleUser
|
||||
if req.Role == models.UserRoleAdmin {
|
||||
role = models.UserRoleAdmin
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
Username: strings.ToLower(req.Username),
|
||||
Email: strings.ToLower(req.Email),
|
||||
PasswordHash: string(hash),
|
||||
Role: role,
|
||||
IsActive: true,
|
||||
Handle: auth.UniqueHandle(c.Request.Context(), h.stores.Users, models.HandleFromName(req.Username)),
|
||||
Handle: auth.UniqueHandle(ctx, h.stores.Users, models.HandleFromName(req.Username)),
|
||||
}
|
||||
|
||||
if err := h.stores.Users.Create(c.Request.Context(), user); err != nil {
|
||||
if err := h.stores.Users.Create(ctx, user); err != nil {
|
||||
if database.IsUniqueViolation(err) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "username or email already exists"})
|
||||
return
|
||||
@@ -100,8 +96,10 @@ func (h *AdminHandler) CreateUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Sync Admins group membership with assigned role.
|
||||
auth.SyncAdminsGroupMembership(c.Request.Context(), h.stores, user.ID, role)
|
||||
auth.EnsureEveryoneGroup(ctx, h.stores, user.ID)
|
||||
if req.IsAdmin {
|
||||
auth.AddToAdminsGroup(ctx, h.stores, user.ID)
|
||||
}
|
||||
|
||||
h.auditLog(c, "user.create", "user", user.ID, nil)
|
||||
c.JSON(http.StatusCreated, user)
|
||||
@@ -111,42 +109,42 @@ func (h *AdminHandler) UpdateUserRole(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
ctx := c.Request.Context()
|
||||
var req struct {
|
||||
Role string `json:"role" binding:"required"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.stores.Users.GetByID(ctx, id)
|
||||
if err != nil || user == nil {
|
||||
if _, err := h.stores.Users.GetByID(ctx, id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Guard: prevent demoting the last admin.
|
||||
// Count Admins group members — the source of truth for admin access.
|
||||
if req.Role != models.UserRoleAdmin && user.Role == models.UserRoleAdmin {
|
||||
if !req.IsAdmin {
|
||||
// Guard: prevent removing the last admin from Admins group.
|
||||
members, _ := h.stores.Groups.ListMembers(ctx, auth.AdminsGroupID)
|
||||
if len(members) <= 1 {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "cannot demote the last admin"})
|
||||
isMember := false
|
||||
for _, m := range members {
|
||||
if m.UserID == id {
|
||||
isMember = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isMember && len(members) <= 1 {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "cannot remove the last admin"})
|
||||
return
|
||||
}
|
||||
auth.RemoveFromAdminsGroup(ctx, h.stores, id)
|
||||
} else {
|
||||
auth.AddToAdminsGroup(ctx, h.stores, id)
|
||||
}
|
||||
|
||||
if err := h.stores.Users.Update(ctx, id, map[string]interface{}{"role": req.Role}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update role"})
|
||||
return
|
||||
}
|
||||
|
||||
// Sync Admins group membership with role change.
|
||||
auth.SyncAdminsGroupMembership(ctx, h.stores, id, req.Role)
|
||||
|
||||
h.auditLog(c, "user.role_change", "user", id, gin.H{"role": req.Role})
|
||||
h.auditLog(c, "user.role_change", "user", id, gin.H{"is_admin": req.IsAdmin})
|
||||
if h.onUserChanged != nil {
|
||||
h.onUserChanged(id)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "role updated"})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "admin status updated"})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ToggleUserActive(c *gin.Context) {
|
||||
@@ -255,18 +253,22 @@ func (h *AdminHandler) DeleteUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
// Guard: prevent deleting the last admin
|
||||
user, err := h.stores.Users.GetByID(c.Request.Context(), id)
|
||||
if err != nil || user == nil {
|
||||
if _, err := h.stores.Users.GetByID(c.Request.Context(), id); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||
return
|
||||
}
|
||||
if user.Role == models.UserRoleAdmin {
|
||||
members, _ := h.stores.Groups.ListMembers(c.Request.Context(), auth.AdminsGroupID)
|
||||
if len(members) <= 1 {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "cannot delete the last admin"})
|
||||
return
|
||||
members, _ := h.stores.Groups.ListMembers(c.Request.Context(), auth.AdminsGroupID)
|
||||
isAdmin := false
|
||||
for _, m := range members {
|
||||
if m.UserID == id {
|
||||
isAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isAdmin && len(members) <= 1 {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "cannot delete the last admin"})
|
||||
return
|
||||
}
|
||||
|
||||
// Destroy vault and personal BYOK keys before deleting the user row
|
||||
h.destroyVault(c, id)
|
||||
|
||||
@@ -32,7 +32,6 @@ const bcryptCost = 12
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -295,7 +294,6 @@ func (h *AuthHandler) generateTokens(user *models.User) (gin.H, error) {
|
||||
accessClaims := Claims{
|
||||
UserID: user.ID,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
@@ -327,7 +325,6 @@ func (h *AuthHandler) generateTokens(user *models.User) (gin.H, error) {
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"display_name": user.DisplayName,
|
||||
"role": user.Role,
|
||||
"handle": user.Handle,
|
||||
"auth_source": user.AuthSource,
|
||||
},
|
||||
@@ -489,10 +486,9 @@ func BootstrapAdmin(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKC
|
||||
|
||||
existing, _ := s.Users.GetByUsername(ctx, cfg.AdminUsername)
|
||||
if existing != nil {
|
||||
// Update password and ensure admin role
|
||||
// Update password
|
||||
s.Users.Update(ctx, existing.ID, map[string]interface{}{
|
||||
"password_hash": string(hash),
|
||||
"role": models.UserRoleAdmin,
|
||||
"is_active": true,
|
||||
})
|
||||
// If the actual password changed, the vault seal is stale. Probe it
|
||||
@@ -507,7 +503,8 @@ func BootstrapAdmin(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKC
|
||||
cache = uekCache[0]
|
||||
}
|
||||
ProbeAndRepairVault(ctx, s, existing.ID, cfg.AdminPassword, cache)
|
||||
ensureAdminsGroupMember(ctx, s, existing.ID)
|
||||
auth.EnsureEveryoneGroup(ctx, s, existing.ID)
|
||||
auth.AddToAdminsGroup(ctx, s, existing.ID)
|
||||
log.Printf(" ✅ Admin user '%s' updated", cfg.AdminUsername)
|
||||
return
|
||||
}
|
||||
@@ -522,7 +519,6 @@ func BootstrapAdmin(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKC
|
||||
Username: strings.ToLower(cfg.AdminUsername),
|
||||
Email: strings.ToLower(email),
|
||||
PasswordHash: string(hash),
|
||||
Role: models.UserRoleAdmin,
|
||||
IsActive: true,
|
||||
AuthSource: "builtin",
|
||||
Handle: handle,
|
||||
@@ -532,13 +528,14 @@ func BootstrapAdmin(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKC
|
||||
log.Printf("⚠ Failed to create admin user: %v", err)
|
||||
return
|
||||
}
|
||||
ensureAdminsGroupMember(ctx, s, user.ID)
|
||||
auth.EnsureEveryoneGroup(ctx, s, user.ID)
|
||||
auth.AddToAdminsGroup(ctx, s, user.ID)
|
||||
log.Printf(" ✅ Admin user '%s' created", cfg.AdminUsername)
|
||||
}
|
||||
|
||||
// SeedUsers creates or updates users from the SEED_USERS env var.
|
||||
// Format: "user:pass:role,user2:pass2:role2" where role is "admin" or "user".
|
||||
// Upsert: existing users get their password and role refreshed on every restart.
|
||||
// Format: "user:pass[:admin],user2:pass2" — third field "admin" adds to Admins group.
|
||||
// Upsert: existing users get their password refreshed on every restart.
|
||||
// Gated to non-production environments.
|
||||
func SeedUsers(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKCache) {
|
||||
if cfg.SeedUsers == "" {
|
||||
@@ -563,16 +560,13 @@ func SeedUsers(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKCache)
|
||||
|
||||
parts := strings.SplitN(entry, ":", 3)
|
||||
if len(parts) < 2 {
|
||||
log.Printf("⚠ Seed user skipped (bad format, want user:pass[:role]): %q", entry)
|
||||
log.Printf("⚠ Seed user skipped (bad format, want user:pass[:admin]): %q", entry)
|
||||
continue
|
||||
}
|
||||
|
||||
username := strings.ToLower(strings.TrimSpace(parts[0]))
|
||||
password := strings.TrimSpace(parts[1])
|
||||
role := models.UserRoleUser
|
||||
if len(parts) >= 3 && strings.TrimSpace(parts[2]) == "admin" {
|
||||
role = models.UserRoleAdmin
|
||||
}
|
||||
isAdmin := len(parts) >= 3 && strings.TrimSpace(parts[2]) == "admin"
|
||||
|
||||
if username == "" || password == "" {
|
||||
log.Printf("⚠ Seed user skipped (empty username or password)")
|
||||
@@ -585,15 +579,13 @@ func SeedUsers(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKCache)
|
||||
continue
|
||||
}
|
||||
|
||||
// Upsert: update password+role if user exists, create if not
|
||||
// Upsert: update password if user exists, create if not
|
||||
existing, _ := s.Users.GetByUsername(ctx, username)
|
||||
if existing != nil {
|
||||
s.Users.Update(ctx, existing.ID, map[string]interface{}{
|
||||
"password_hash": string(hash),
|
||||
"role": role,
|
||||
"is_active": true,
|
||||
})
|
||||
// Probe vault with current password — only destroys if seal is stale
|
||||
if existing.Handle == "" {
|
||||
handle := auth.UniqueHandle(ctx, s.Users, models.HandleFromName(username))
|
||||
s.Users.Update(ctx, existing.ID, map[string]interface{}{"handle": handle})
|
||||
@@ -603,10 +595,11 @@ func SeedUsers(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKCache)
|
||||
cache = uekCache[0]
|
||||
}
|
||||
ProbeAndRepairVault(ctx, s, existing.ID, password, cache)
|
||||
if role == models.UserRoleAdmin {
|
||||
ensureAdminsGroupMember(ctx, s, existing.ID)
|
||||
auth.EnsureEveryoneGroup(ctx, s, existing.ID)
|
||||
if isAdmin {
|
||||
auth.AddToAdminsGroup(ctx, s, existing.ID)
|
||||
}
|
||||
log.Printf(" 🌱 Seed user '%s' updated (role=%s)", username, role)
|
||||
log.Printf(" 🌱 Seed user '%s' updated (admin=%v)", username, isAdmin)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -615,7 +608,6 @@ func SeedUsers(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKCache)
|
||||
Username: username,
|
||||
Email: username + "@switchboard.local",
|
||||
PasswordHash: string(hash),
|
||||
Role: role,
|
||||
IsActive: true,
|
||||
AuthSource: "builtin",
|
||||
Handle: handle,
|
||||
@@ -625,20 +617,14 @@ func SeedUsers(cfg *config.Config, s store.Stores, uekCache ...*crypto.UEKCache)
|
||||
log.Printf("⚠ Seed user '%s' failed: %v", username, err)
|
||||
continue
|
||||
}
|
||||
if role == models.UserRoleAdmin {
|
||||
ensureAdminsGroupMember(ctx, s, user.ID)
|
||||
auth.EnsureEveryoneGroup(ctx, s, user.ID)
|
||||
if isAdmin {
|
||||
auth.AddToAdminsGroup(ctx, s, user.ID)
|
||||
}
|
||||
log.Printf(" 🌱 Seed user '%s' created (role=%s, active=true)", username, role)
|
||||
log.Printf(" 🌱 Seed user '%s' created (admin=%v, active=true)", username, isAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
// ensureAdminsGroupMember adds userID to the Admins system group (idempotent).
|
||||
// Used by BootstrapAdmin and SeedUsers so that admin users receive
|
||||
// surface.admin.access through the normal permission resolution path.
|
||||
func ensureAdminsGroupMember(ctx context.Context, s store.Stores, userID string) {
|
||||
auth.SyncAdminsGroupMembership(ctx, s, userID, models.UserRoleAdmin)
|
||||
}
|
||||
|
||||
// IsRegistrationEnabled checks the platform policy.
|
||||
func IsRegistrationEnabled(s store.Stores) bool {
|
||||
val, _ := s.Policies.GetBool(context.Background(), "allow_registration")
|
||||
|
||||
@@ -40,7 +40,6 @@ func TestJWTGeneration(t *testing.T) {
|
||||
claims := Claims{
|
||||
UserID: "550e8400-e29b-41d4-a716-446655440000",
|
||||
Email: "test@example.com",
|
||||
Role: "user",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(15 * time.Minute)),
|
||||
@@ -75,9 +74,6 @@ func TestJWTGeneration(t *testing.T) {
|
||||
if parsed.Email != claims.Email {
|
||||
t.Errorf("Email mismatch: got %s, want %s", parsed.Email, claims.Email)
|
||||
}
|
||||
if parsed.Role != claims.Role {
|
||||
t.Errorf("Role mismatch: got %s, want %s", parsed.Role, claims.Role)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTExpired(t *testing.T) {
|
||||
@@ -86,7 +82,6 @@ func TestJWTExpired(t *testing.T) {
|
||||
claims := Claims{
|
||||
UserID: "test-user",
|
||||
Email: "test@example.com",
|
||||
Role: "user",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(-30 * time.Minute)),
|
||||
@@ -109,7 +104,6 @@ func TestJWTWrongSecret(t *testing.T) {
|
||||
claims := Claims{
|
||||
UserID: "test-user",
|
||||
Email: "test@example.com",
|
||||
Role: "user",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
|
||||
},
|
||||
|
||||
@@ -81,17 +81,19 @@ func setupExtensionHarness(t *testing.T) *extensionHarness {
|
||||
|
||||
// Seed admin user
|
||||
adminID := seedInsertReturningID(t,
|
||||
`INSERT INTO users (username, email, password_hash, role, handle, auth_source) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
|
||||
"ext-admin", "ext-admin@test.com", "$2a$10$test", "admin", "ext-admin", "builtin",
|
||||
`INSERT INTO users (username, email, password_hash, handle, auth_source) VALUES ($1, $2, $3, $4, $5) RETURNING id`,
|
||||
"ext-admin", "ext-admin@test.com", "$2a$10$test", "ext-admin", "builtin",
|
||||
)
|
||||
database.SeedEveryoneGroupMember(t, adminID)
|
||||
database.SeedAdminsGroupMember(t, adminID)
|
||||
adminToken := makeToken(adminID, "ext-admin@test.com", "admin")
|
||||
|
||||
// Seed regular user
|
||||
userID := seedInsertReturningID(t,
|
||||
`INSERT INTO users (username, email, password_hash, role, handle, auth_source) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
|
||||
"ext-user", "ext-user@test.com", "$2a$10$test", "user", "ext-user", "builtin",
|
||||
`INSERT INTO users (username, email, password_hash, handle, auth_source) VALUES ($1, $2, $3, $4, $5) RETURNING id`,
|
||||
"ext-user", "ext-user@test.com", "$2a$10$test", "ext-user", "builtin",
|
||||
)
|
||||
database.SeedEveryoneGroupMember(t, userID)
|
||||
userToken := makeToken(userID, "ext-user@test.com", "user")
|
||||
|
||||
return &extensionHarness{
|
||||
|
||||
@@ -74,7 +74,7 @@ func setupNotifHarness(t *testing.T) *notifHarness {
|
||||
|
||||
// Seed admin
|
||||
adminID := database.SeedTestUser(t, "notif-admin", "notif-admin@test.com")
|
||||
database.TestDB.Exec(dialectSQL("UPDATE users SET role = 'admin', is_active = true WHERE id = $1"), adminID)
|
||||
database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), adminID)
|
||||
database.SeedAdminsGroupMember(t, adminID)
|
||||
adminToken := makeToken(adminID, "notif-admin@test.com", "admin")
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ func NewProfileBootstrapHandler(s store.Stores) *ProfileBootstrapHandler {
|
||||
// GET /api/v1/profile/bootstrap
|
||||
func (h *ProfileBootstrapHandler) GetBootstrap(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
role, _ := c.Get("role")
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// ── User profile ────────────────────────
|
||||
@@ -48,27 +47,20 @@ func (h *ProfileBootstrapHandler) GetBootstrap(c *gin.Context) {
|
||||
"username": user.Username,
|
||||
"display_name": user.DisplayName,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
}
|
||||
if user.AvatarURL != "" {
|
||||
userPayload["avatar"] = user.AvatarURL
|
||||
}
|
||||
|
||||
// ── Permissions ─────────────────────────
|
||||
var permList []string
|
||||
if role == "admin" {
|
||||
permList = make([]string, len(auth.AllPermissions))
|
||||
copy(permList, auth.AllPermissions)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -77,7 +69,6 @@ func (h *ProfileBootstrapHandler) GetBootstrap(c *gin.Context) {
|
||||
if groupIDs == nil {
|
||||
groupIDs = []string{}
|
||||
}
|
||||
groupIDs = append(groupIDs, auth.EveryoneGroupID)
|
||||
|
||||
// ── Teams ───────────────────────────────
|
||||
teams, _ := h.stores.Teams.ListForUser(ctx, userID)
|
||||
|
||||
@@ -36,7 +36,6 @@ type profileResponse struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
Role string `json:"role"`
|
||||
Avatar *string `json:"avatar,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
@@ -84,7 +83,6 @@ func (h *SettingsHandler) GetProfile(c *gin.Context) {
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
DisplayName: dn,
|
||||
Role: user.Role,
|
||||
Avatar: avatar,
|
||||
Settings: settings,
|
||||
CreatedAt: user.CreatedAt,
|
||||
|
||||
@@ -41,11 +41,10 @@ func (h *testHarness) request(method, path, token string, body interface{}) *htt
|
||||
return w
|
||||
}
|
||||
|
||||
func makeToken(userID, email, role string) string {
|
||||
func makeToken(userID, email, _ string) string {
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
|
||||
|
||||
Reference in New Issue
Block a user