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": []
}
],
"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": [
"application/json"
],
@ -526,7 +526,7 @@ const docTemplate = `{
"parameters": [
{
"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",
"in": "path",
"required": true
@ -559,6 +559,12 @@ const docTemplate = `{
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
@ -575,7 +581,7 @@ const docTemplate = `{
"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": [
"application/json"
],
@ -589,7 +595,7 @@ const docTemplate = `{
"parameters": [
{
"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",
"in": "path",
"required": true
@ -622,6 +628,12 @@ const docTemplate = `{
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {

View File

@ -504,7 +504,7 @@
"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": [
"application/json"
],
@ -518,7 +518,7 @@
"parameters": [
{
"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",
"in": "path",
"required": true
@ -551,6 +551,12 @@
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
@ -567,7 +573,7 @@
"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": [
"application/json"
],
@ -581,7 +587,7 @@
"parameters": [
{
"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",
"in": "path",
"required": true
@ -614,6 +620,12 @@
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {

View File

@ -2879,14 +2879,14 @@ paths:
- application/json
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).
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).
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
name: role
required: true
@ -2911,6 +2911,10 @@ paths:
description: Forbidden
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
@ -2927,12 +2931,14 @@ paths:
- application/json
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.
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.
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
name: role
required: true
@ -2957,6 +2963,10 @@ paths:
description: Forbidden
schema:
$ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:

View File

@ -2,6 +2,7 @@ package domain
// BulkAccountsByRoleRequest is optional JSON for POST /admin/roles/:role/bulk-{deactivate,reactivate}.
// 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 {
ExcludeTeamMemberID *int64 `json:"exclude_team_member_id,omitempty"`
}

View File

@ -6,12 +6,14 @@ import (
"Yimaru-Backend/internal/web_server/response"
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/jackc/pgx/v5"
"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)
}
// 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
// @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
// @Accept json
// @Produce json
// @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"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 403 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/admin/roles/{role}/bulk-deactivate [post]
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")))
if roleKey == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role",
Error: "role path parameter is required",
})
roleKey, rpOK := h.bulkAccountsRoleFromPath(c)
if !rpOK {
return nil
}
if roleKey == string(domain.RoleSuperAdmin) || roleKey == string(domain.TeamRoleSuperAdmin) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
@ -509,16 +547,17 @@ func (h *Handler) BulkDeactivateAccountsByRole(c *fiber.Ctx) error {
// BulkReactivateAccountsByRole godoc
// @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
// @Accept json
// @Produce json
// @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"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 403 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/admin/roles/{role}/bulk-reactivate [post]
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")))
if roleKey == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role",
Error: "role path parameter is required",
})
roleKey, rpOK := h.bulkAccountsRoleFromPath(c)
if !rpOK {
return nil
}
if roleKey == string(domain.RoleSuperAdmin) || roleKey == string(domain.TeamRoleSuperAdmin) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{