diff --git a/.cursor/rules/git-push-commit-if-dirty.mdc b/.cursor/rules/git-push-commit-if-dirty.mdc new file mode 100644 index 0000000..45ea08b --- /dev/null +++ b/.cursor/rules/git-push-commit-if-dirty.mdc @@ -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 repo’s 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. diff --git a/docs/docs.go b/docs/docs.go index 4f0bc0a..639a6cc 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": { diff --git a/docs/swagger.json b/docs/swagger.json index f3c3d48..fb4f445 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 366afbd..3f08e03 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: diff --git a/internal/domain/bulk_deactivate_accounts.go b/internal/domain/bulk_deactivate_accounts.go index 1fc8f8d..ae826a4 100644 --- a/internal/domain/bulk_deactivate_accounts.go +++ b/internal/domain/bulk_deactivate_accounts.go @@ -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"` } diff --git a/internal/web_server/handlers/admin.go b/internal/web_server/handlers/admin.go index ed7cc99..d6c6fe0 100644 --- a/internal/web_server/handlers/admin.go +++ b/internal/web_server/handlers/admin.go @@ -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{