Resolve bulk role path segment from RBAC roles.id.

Admin bulk deactivate/reactivate accepts decimal path segments matching GET /rbac/roles IDs, resolving RoleRecord.name to the platform key. Document 404 when id is unknown. Add Cursor rule: on push, commit dirty tree first without secrets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-05-19 01:16:28 -07:00
parent 2f73b60122
commit 4a681265d7
6 changed files with 121 additions and 36 deletions

View File

@ -0,0 +1,14 @@
---
description: Commit before push whenever the tree is dirty
alwaysApply: true
---
# Git push
When the user asks to push (including phrases like “push”, “push to remote”, or “git push”):
1. Run `git status` (and if needed `git diff`) to check for unstaged/uncommitted changes.
2. If there are changes worth shipping, stage and **commit first**—never omit secrets such as `.env`, credentials files, or private keys. Follow the repos commit message conventions.
3. Then run `git push` to the tracked upstream.
If nothing is staged and the working tree is clean, pushing without a commit is fine.

View File

@ -512,7 +512,7 @@ const docTemplate = `{
"Bearer": [] "Bearer": []
} }
], ],
"description": "Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. The path role must match a valid learner role or team role string (e.g. STUDENT, INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN users (team_members with team_role ADMIN under path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).", "description": "Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. Path :role may be a role key (e.g. INSTRUCTOR, ADMIN) or a decimal RBAC roles.id from GET /api/v1/rbac/roles (resolved to RoleRecord.name uppercased). SUPER_ADMIN cannot be bulk-deactivated. ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN users (team_members with team_role ADMIN under path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -526,7 +526,7 @@ const docTemplate = `{
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Role key (matches users.role and/or team_members.team_role)", "description": "Role key (INSTRUCTOR etc.) or RBAC roles.id (integer string)",
"name": "role", "name": "role",
"in": "path", "in": "path",
"required": true "required": true
@ -559,6 +559,12 @@ const docTemplate = `{
"$ref": "#/definitions/domain.ErrorResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
@ -575,7 +581,7 @@ const docTemplate = `{
"Bearer": [] "Bearer": []
} }
], ],
"description": "Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN users (team_members ADMIN under path ADMIN is allowed). Matches only users currently DEACTIVATED and team rows currently inactive.", "description": "Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path :role may be a role key or decimal RBAC roles.id (see bulk-deactivate). Path role must correspond to valid platform users.role or team_members.team_role (after resolving id → name). SUPER_ADMIN cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN users (team_members ADMIN under path ADMIN is allowed). Matches only users currently DEACTIVATED and team rows currently inactive.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -589,7 +595,7 @@ const docTemplate = `{
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Role key (matches users.role and/or team_members.team_role)", "description": "Role key (INSTRUCTOR etc.) or RBAC roles.id (integer string)",
"name": "role", "name": "role",
"in": "path", "in": "path",
"required": true "required": true
@ -622,6 +628,12 @@ const docTemplate = `{
"$ref": "#/definitions/domain.ErrorResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {

View File

@ -504,7 +504,7 @@
"Bearer": [] "Bearer": []
} }
], ],
"description": "Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. The path role must match a valid learner role or team role string (e.g. STUDENT, INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN users (team_members with team_role ADMIN under path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).", "description": "Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. Path :role may be a role key (e.g. INSTRUCTOR, ADMIN) or a decimal RBAC roles.id from GET /api/v1/rbac/roles (resolved to RoleRecord.name uppercased). SUPER_ADMIN cannot be bulk-deactivated. ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN users (team_members with team_role ADMIN under path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -518,7 +518,7 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Role key (matches users.role and/or team_members.team_role)", "description": "Role key (INSTRUCTOR etc.) or RBAC roles.id (integer string)",
"name": "role", "name": "role",
"in": "path", "in": "path",
"required": true "required": true
@ -551,6 +551,12 @@
"$ref": "#/definitions/domain.ErrorResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
@ -567,7 +573,7 @@
"Bearer": [] "Bearer": []
} }
], ],
"description": "Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN users (team_members ADMIN under path ADMIN is allowed). Matches only users currently DEACTIVATED and team rows currently inactive.", "description": "Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path :role may be a role key or decimal RBAC roles.id (see bulk-deactivate). Path role must correspond to valid platform users.role or team_members.team_role (after resolving id → name). SUPER_ADMIN cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN users (team_members ADMIN under path ADMIN is allowed). Matches only users currently DEACTIVATED and team rows currently inactive.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -581,7 +587,7 @@
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
"description": "Role key (matches users.role and/or team_members.team_role)", "description": "Role key (INSTRUCTOR etc.) or RBAC roles.id (integer string)",
"name": "role", "name": "role",
"in": "path", "in": "path",
"required": true "required": true
@ -614,6 +620,12 @@
"$ref": "#/definitions/domain.ErrorResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {

View File

@ -2879,14 +2879,14 @@ paths:
- application/json - application/json
description: Sets all platform users with the given users.role to DEACTIVATED description: Sets all platform users with the given users.role to DEACTIVATED
(except the caller) and all team_members with the given team_role to inactive. (except the caller) and all team_members with the given team_role to inactive.
The path role must match a valid learner role or team role string (e.g. STUDENT, Path :role may be a role key (e.g. INSTRUCTOR, ADMIN) or a decimal RBAC roles.id
INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. from GET /api/v1/rbac/roles (resolved to RoleRecord.name uppercased). SUPER_ADMIN
ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN cannot be bulk-deactivated. ADMIN platform users must use SUPER_ADMIN to bulk
users (team_members with team_role ADMIN under path ADMIN remain allowed). change other platform ADMIN users (team_members with team_role ADMIN under
Empty body allowed; optionally pass exclude_team_member_id to skip one team_members path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id
row (e.g. yourself). to skip one team_members row (e.g. yourself).
parameters: parameters:
- description: Role key (matches users.role and/or team_members.team_role) - description: Role key (INSTRUCTOR etc.) or RBAC roles.id (integer string)
in: path in: path
name: role name: role
required: true required: true
@ -2911,6 +2911,10 @@ paths:
description: Forbidden description: Forbidden
schema: schema:
$ref: '#/definitions/domain.ErrorResponse' $ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500": "500":
description: Internal Server Error description: Internal Server Error
schema: schema:
@ -2927,12 +2931,14 @@ paths:
- application/json - application/json
description: Sets all platform users with the given role from DEACTIVATED to description: Sets all platform users with the given role from DEACTIVATED to
ACTIVE (except the caller) and all team_members with the given team_role from ACTIVE (except the caller) and all team_members with the given team_role from
inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN inactive to active. Path :role may be a role key or decimal RBAC roles.id
cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN (see bulk-deactivate). Path role must correspond to valid platform users.role
users (team_members ADMIN under path ADMIN is allowed). Matches only users or team_members.team_role (after resolving id → name). SUPER_ADMIN cannot
currently DEACTIVATED and team rows currently inactive. be bulk changed. ADMIN callers cannot bulk change other platform ADMIN users
(team_members ADMIN under path ADMIN is allowed). Matches only users currently
DEACTIVATED and team rows currently inactive.
parameters: parameters:
- description: Role key (matches users.role and/or team_members.team_role) - description: Role key (INSTRUCTOR etc.) or RBAC roles.id (integer string)
in: path in: path
name: role name: role
required: true required: true
@ -2957,6 +2963,10 @@ paths:
description: Forbidden description: Forbidden
schema: schema:
$ref: '#/definitions/domain.ErrorResponse' $ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500": "500":
description: Internal Server Error description: Internal Server Error
schema: schema:

View File

@ -2,6 +2,7 @@ package domain
// BulkAccountsByRoleRequest is optional JSON for POST /admin/roles/:role/bulk-{deactivate,reactivate}. // BulkAccountsByRoleRequest is optional JSON for POST /admin/roles/:role/bulk-{deactivate,reactivate}.
// Optional exclusions apply to team_members bulk updates only. // Optional exclusions apply to team_members bulk updates only.
// Path :role is a platform/team role key or a numeric RBAC roles.id string (decimal digits resolve via GET /api/v1/rbac/roles ids).
type BulkAccountsByRoleRequest struct { type BulkAccountsByRoleRequest struct {
ExcludeTeamMemberID *int64 `json:"exclude_team_member_id,omitempty"` ExcludeTeamMemberID *int64 `json:"exclude_team_member_id,omitempty"`
} }

View File

@ -6,12 +6,14 @@ import (
"Yimaru-Backend/internal/web_server/response" "Yimaru-Backend/internal/web_server/response"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/jackc/pgx/v5"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -381,18 +383,57 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Admin updated successfully", nil, nil) return response.WriteJSON(c, fiber.StatusOK, "Admin updated successfully", nil, nil)
} }
// bulkAccountsRoleFromPath resolves admin bulk :role: decimal digits → rbac roles.id lookup (same ids as GET /api/v1/rbac/roles); otherwise uppercase role key.
func (h *Handler) bulkAccountsRoleFromPath(c *fiber.Ctx) (roleKey string, ok bool) {
raw := strings.TrimSpace(c.Params("role"))
if raw == "" {
_ = c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role",
Error: "role path parameter is required",
})
return "", false
}
if rbacID, parseErr := strconv.ParseInt(raw, 10, 64); parseErr == nil {
if rbacID <= 0 {
_ = c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role",
Error: "numeric role path must be a positive RBAC roles.id (see GET /api/v1/rbac/roles)",
})
return "", false
}
rec, err := h.rbacSvc.GetRoleByID(c.Context(), rbacID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
_ = c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Invalid role",
Error: "RBAC role id not found",
})
return "", false
}
_ = c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "RBAC lookup failed",
Error: err.Error(),
})
return "", false
}
return strings.ToUpper(strings.TrimSpace(rec.Name)), true
}
return strings.ToUpper(raw), true
}
// BulkDeactivateAccountsByRole godoc // BulkDeactivateAccountsByRole godoc
// @Summary Bulk deactivate accounts by role (SUPER_ADMIN or ADMIN platform users only) // @Summary Bulk deactivate accounts by role (SUPER_ADMIN or ADMIN platform users only)
// @Description Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. The path role must match a valid learner role or team role string (e.g. STUDENT, INSTRUCTOR, ADMIN, CONTENT_MANAGER). SUPER_ADMIN cannot be bulk-deactivated. ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN users (team_members with team_role ADMIN under path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself). // @Description Sets all platform users with the given users.role to DEACTIVATED (except the caller) and all team_members with the given team_role to inactive. Path :role may be a role key (e.g. INSTRUCTOR, ADMIN) or a decimal RBAC roles.id from GET /api/v1/rbac/roles (resolved to RoleRecord.name uppercased). SUPER_ADMIN cannot be bulk-deactivated. ADMIN platform users must use SUPER_ADMIN to bulk change other platform ADMIN users (team_members with team_role ADMIN under path ADMIN remain allowed). Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).
// @Tags admin // @Tags admin
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security Bearer // @Security Bearer
// @Param role path string true "Role key (matches users.role and/or team_members.team_role)" // @Param role path string true "Role key (INSTRUCTOR etc.) or RBAC roles.id (integer string)"
// @Param body body domain.BulkAccountsByRoleRequest false "Optional exclusions" // @Param body body domain.BulkAccountsByRoleRequest false "Optional exclusions"
// @Success 200 {object} domain.Response // @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 403 {object} domain.ErrorResponse // @Failure 403 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/admin/roles/{role}/bulk-deactivate [post] // @Router /api/v1/admin/roles/{role}/bulk-deactivate [post]
func (h *Handler) BulkDeactivateAccountsByRole(c *fiber.Ctx) error { func (h *Handler) BulkDeactivateAccountsByRole(c *fiber.Ctx) error {
@ -418,12 +459,9 @@ func (h *Handler) BulkDeactivateAccountsByRole(c *fiber.Ctx) error {
}) })
} }
roleKey := strings.ToUpper(strings.TrimSpace(c.Params("role"))) roleKey, rpOK := h.bulkAccountsRoleFromPath(c)
if roleKey == "" { if !rpOK {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return nil
Message: "Invalid role",
Error: "role path parameter is required",
})
} }
if roleKey == string(domain.RoleSuperAdmin) || roleKey == string(domain.TeamRoleSuperAdmin) { if roleKey == string(domain.RoleSuperAdmin) || roleKey == string(domain.TeamRoleSuperAdmin) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
@ -509,16 +547,17 @@ func (h *Handler) BulkDeactivateAccountsByRole(c *fiber.Ctx) error {
// BulkReactivateAccountsByRole godoc // BulkReactivateAccountsByRole godoc
// @Summary Bulk reactivate accounts by role (SUPER_ADMIN or ADMIN platform users only) // @Summary Bulk reactivate accounts by role (SUPER_ADMIN or ADMIN platform users only)
// @Description Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path role must be a valid platform or team role. SUPER_ADMIN cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN users (team_members ADMIN under path ADMIN is allowed). Matches only users currently DEACTIVATED and team rows currently inactive. // @Description Sets all platform users with the given role from DEACTIVATED to ACTIVE (except the caller) and all team_members with the given team_role from inactive to active. Path :role may be a role key or decimal RBAC roles.id (see bulk-deactivate). Path role must correspond to valid platform users.role or team_members.team_role (after resolving id → name). SUPER_ADMIN cannot be bulk changed. ADMIN callers cannot bulk change other platform ADMIN users (team_members ADMIN under path ADMIN is allowed). Matches only users currently DEACTIVATED and team rows currently inactive.
// @Tags admin // @Tags admin
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security Bearer // @Security Bearer
// @Param role path string true "Role key (matches users.role and/or team_members.team_role)" // @Param role path string true "Role key (INSTRUCTOR etc.) or RBAC roles.id (integer string)"
// @Param body body domain.BulkAccountsByRoleRequest false "Optional exclusions" // @Param body body domain.BulkAccountsByRoleRequest false "Optional exclusions"
// @Success 200 {object} domain.Response // @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 403 {object} domain.ErrorResponse // @Failure 403 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/admin/roles/{role}/bulk-reactivate [post] // @Router /api/v1/admin/roles/{role}/bulk-reactivate [post]
func (h *Handler) BulkReactivateAccountsByRole(c *fiber.Ctx) error { func (h *Handler) BulkReactivateAccountsByRole(c *fiber.Ctx) error {
@ -544,12 +583,9 @@ func (h *Handler) BulkReactivateAccountsByRole(c *fiber.Ctx) error {
}) })
} }
roleKey := strings.ToUpper(strings.TrimSpace(c.Params("role"))) roleKey, rpOK := h.bulkAccountsRoleFromPath(c)
if roleKey == "" { if !rpOK {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return nil
Message: "Invalid role",
Error: "role path parameter is required",
})
} }
if roleKey == string(domain.RoleSuperAdmin) || roleKey == string(domain.TeamRoleSuperAdmin) { if roleKey == string(domain.RoleSuperAdmin) || roleKey == string(domain.TeamRoleSuperAdmin) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{