Add SUPER_ADMIN bulk deactivate and reactivate by role.
Expose POST /admin/roles/:role/bulk-deactivate and bulk-reactivate for platform users and team_members, mirroring deactivate/reactivate semantics and optional team member exclusions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a80db8afd9
commit
ecad91d89e
|
|
@ -140,6 +140,32 @@ WHERE id = $1;
|
|||
DELETE FROM team_members
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: BulkDeactivateTeamMembersByRole :execrows
|
||||
UPDATE team_members
|
||||
SET
|
||||
status = 'inactive',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
team_role = $1
|
||||
AND (
|
||||
sqlc.narg('exclude_team_member_id')::BIGINT IS NULL
|
||||
OR id <> sqlc.narg('exclude_team_member_id')::BIGINT
|
||||
)
|
||||
AND status = 'active';
|
||||
|
||||
-- name: BulkReactivateTeamMembersByRole :execrows
|
||||
UPDATE team_members
|
||||
SET
|
||||
status = 'active',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
team_role = $1
|
||||
AND (
|
||||
sqlc.narg('exclude_team_member_id')::BIGINT IS NULL
|
||||
OR id <> sqlc.narg('exclude_team_member_id')::BIGINT
|
||||
)
|
||||
AND status = 'inactive';
|
||||
|
||||
-- name: CheckTeamMemberEmailExists :one
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM team_members WHERE email = $1
|
||||
|
|
|
|||
|
|
@ -421,6 +421,26 @@ SET
|
|||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2;
|
||||
|
||||
-- name: BulkDeactivateUsersByRole :execrows
|
||||
UPDATE users
|
||||
SET
|
||||
status = 'DEACTIVATED',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
role = $1
|
||||
AND id <> $2
|
||||
AND status <> 'DEACTIVATED';
|
||||
|
||||
-- name: BulkReactivateUsersByRole :execrows
|
||||
UPDATE users
|
||||
SET
|
||||
status = 'ACTIVE',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
role = $1
|
||||
AND id <> $2
|
||||
AND status = 'DEACTIVATED';
|
||||
|
||||
-- name: GetUserSummary :one
|
||||
SELECT
|
||||
COUNT(*) AS total_users,
|
||||
|
|
|
|||
134
docs/docs.go
134
docs/docs.go
|
|
@ -505,6 +505,132 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/roles/{role}/bulk-deactivate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"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. Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Bulk deactivate accounts by role (SUPER_ADMIN only)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Role key (matches users.role and/or team_members.team_role)",
|
||||
"name": "role",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Optional exclusions",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BulkAccountsByRoleRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/roles/{role}/bulk-reactivate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"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. Matches only users currently DEACTIVATED and team rows currently inactive.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Bulk reactivate accounts by role (SUPER_ADMIN only)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Role key (matches users.role and/or team_members.team_role)",
|
||||
"name": "role",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Optional exclusions",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BulkAccountsByRoleRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/users/deletion-requests": {
|
||||
"get": {
|
||||
"description": "Returns account deletion requests for admin panel tracking with filtering and pagination",
|
||||
|
|
@ -10206,6 +10332,14 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.BulkAccountsByRoleRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exclude_team_member_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.CreateCourseInput": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
|
|||
|
|
@ -497,6 +497,132 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/roles/{role}/bulk-deactivate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"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. Empty body allowed; optionally pass exclude_team_member_id to skip one team_members row (e.g. yourself).",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Bulk deactivate accounts by role (SUPER_ADMIN only)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Role key (matches users.role and/or team_members.team_role)",
|
||||
"name": "role",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Optional exclusions",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BulkAccountsByRoleRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/roles/{role}/bulk-reactivate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"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. Matches only users currently DEACTIVATED and team rows currently inactive.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Bulk reactivate accounts by role (SUPER_ADMIN only)",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Role key (matches users.role and/or team_members.team_role)",
|
||||
"name": "role",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Optional exclusions",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.BulkAccountsByRoleRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/users/deletion-requests": {
|
||||
"get": {
|
||||
"description": "Returns account deletion requests for admin panel tracking with filtering and pagination",
|
||||
|
|
@ -10198,6 +10324,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.BulkAccountsByRoleRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exclude_team_member_id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.CreateCourseInput": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
|
|||
|
|
@ -327,6 +327,11 @@ definitions:
|
|||
total_users:
|
||||
type: integer
|
||||
type: object
|
||||
domain.BulkAccountsByRoleRequest:
|
||||
properties:
|
||||
exclude_team_member_id:
|
||||
type: integer
|
||||
type: object
|
||||
domain.CreateCourseInput:
|
||||
properties:
|
||||
description:
|
||||
|
|
@ -2868,6 +2873,95 @@ paths:
|
|||
summary: Update FAQ
|
||||
tags:
|
||||
- faqs
|
||||
/api/v1/admin/roles/{role}/bulk-deactivate:
|
||||
post:
|
||||
consumes:
|
||||
- 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.
|
||||
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)
|
||||
in: path
|
||||
name: role
|
||||
required: true
|
||||
type: string
|
||||
- description: Optional exclusions
|
||||
in: body
|
||||
name: body
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BulkAccountsByRoleRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"403":
|
||||
description: Forbidden
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Bulk deactivate accounts by role (SUPER_ADMIN only)
|
||||
tags:
|
||||
- admin
|
||||
/api/v1/admin/roles/{role}/bulk-reactivate:
|
||||
post:
|
||||
consumes:
|
||||
- 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. Matches only users currently DEACTIVATED and team
|
||||
rows currently inactive.
|
||||
parameters:
|
||||
- description: Role key (matches users.role and/or team_members.team_role)
|
||||
in: path
|
||||
name: role
|
||||
required: true
|
||||
type: string
|
||||
- description: Optional exclusions
|
||||
in: body
|
||||
name: body
|
||||
schema:
|
||||
$ref: '#/definitions/domain.BulkAccountsByRoleRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/domain.Response'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"403":
|
||||
description: Forbidden
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ErrorResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Bulk reactivate accounts by role (SUPER_ADMIN only)
|
||||
tags:
|
||||
- admin
|
||||
/api/v1/admin/users/{user_id}/lms-learning-activity:
|
||||
get:
|
||||
description: Returns programs, courses, modules, and lessons with completion
|
||||
|
|
|
|||
|
|
@ -11,6 +11,60 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const BulkDeactivateTeamMembersByRole = `-- name: BulkDeactivateTeamMembersByRole :execrows
|
||||
UPDATE team_members
|
||||
SET
|
||||
status = 'inactive',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
team_role = $1
|
||||
AND (
|
||||
$2::BIGINT IS NULL
|
||||
OR id <> $2::BIGINT
|
||||
)
|
||||
AND status = 'active'
|
||||
`
|
||||
|
||||
type BulkDeactivateTeamMembersByRoleParams struct {
|
||||
TeamRole string `json:"team_role"`
|
||||
ExcludeTeamMemberID pgtype.Int8 `json:"exclude_team_member_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) BulkDeactivateTeamMembersByRole(ctx context.Context, arg BulkDeactivateTeamMembersByRoleParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, BulkDeactivateTeamMembersByRole, arg.TeamRole, arg.ExcludeTeamMemberID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const BulkReactivateTeamMembersByRole = `-- name: BulkReactivateTeamMembersByRole :execrows
|
||||
UPDATE team_members
|
||||
SET
|
||||
status = 'active',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
team_role = $1
|
||||
AND (
|
||||
$2::BIGINT IS NULL
|
||||
OR id <> $2::BIGINT
|
||||
)
|
||||
AND status = 'inactive'
|
||||
`
|
||||
|
||||
type BulkReactivateTeamMembersByRoleParams struct {
|
||||
TeamRole string `json:"team_role"`
|
||||
ExcludeTeamMemberID pgtype.Int8 `json:"exclude_team_member_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) BulkReactivateTeamMembersByRole(ctx context.Context, arg BulkReactivateTeamMembersByRoleParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, BulkReactivateTeamMembersByRole, arg.TeamRole, arg.ExcludeTeamMemberID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const CheckTeamMemberEmailExists = `-- name: CheckTeamMemberEmailExists :one
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM team_members WHERE email = $1
|
||||
|
|
|
|||
|
|
@ -11,6 +11,54 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const BulkDeactivateUsersByRole = `-- name: BulkDeactivateUsersByRole :execrows
|
||||
UPDATE users
|
||||
SET
|
||||
status = 'DEACTIVATED',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
role = $1
|
||||
AND id <> $2
|
||||
AND status <> 'DEACTIVATED'
|
||||
`
|
||||
|
||||
type BulkDeactivateUsersByRoleParams struct {
|
||||
Role string `json:"role"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) BulkDeactivateUsersByRole(ctx context.Context, arg BulkDeactivateUsersByRoleParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, BulkDeactivateUsersByRole, arg.Role, arg.ID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const BulkReactivateUsersByRole = `-- name: BulkReactivateUsersByRole :execrows
|
||||
UPDATE users
|
||||
SET
|
||||
status = 'ACTIVE',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
role = $1
|
||||
AND id <> $2
|
||||
AND status = 'DEACTIVATED'
|
||||
`
|
||||
|
||||
type BulkReactivateUsersByRoleParams struct {
|
||||
Role string `json:"role"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) BulkReactivateUsersByRole(ctx context.Context, arg BulkReactivateUsersByRoleParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, BulkReactivateUsersByRole, arg.Role, arg.ID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const CheckPhoneEmailExist = `-- name: CheckPhoneEmailExist :one
|
||||
SELECT
|
||||
EXISTS (
|
||||
|
|
|
|||
27
internal/domain/bulk_deactivate_accounts.go
Normal file
27
internal/domain/bulk_deactivate_accounts.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package domain
|
||||
|
||||
// BulkAccountsByRoleRequest is optional JSON for POST /admin/roles/:role/bulk-{deactivate,reactivate}.
|
||||
// Optional exclusions apply to team_members bulk updates only.
|
||||
type BulkAccountsByRoleRequest struct {
|
||||
ExcludeTeamMemberID *int64 `json:"exclude_team_member_id,omitempty"`
|
||||
}
|
||||
|
||||
// BulkDeactivateAccountsByRoleRequest aliases the shared shape (backward compatible name for Swagger).
|
||||
type BulkDeactivateAccountsByRoleRequest = BulkAccountsByRoleRequest
|
||||
|
||||
// BulkReactivateAccountsByRoleRequest aliases the shared shape.
|
||||
type BulkReactivateAccountsByRoleRequest = BulkAccountsByRoleRequest
|
||||
|
||||
// BulkDeactivateAccountsByRoleResult reports how many rows were updated per table.
|
||||
type BulkDeactivateAccountsByRoleResult struct {
|
||||
Role string `json:"role"`
|
||||
UsersDeactivated int64 `json:"users_deactivated"`
|
||||
TeamMembersDeactivated int64 `json:"team_members_deactivated"`
|
||||
}
|
||||
|
||||
// BulkReactivateAccountsByRoleResult reports how many rows were reactivated per table.
|
||||
type BulkReactivateAccountsByRoleResult struct {
|
||||
Role string `json:"role"`
|
||||
UsersReactivated int64 `json:"users_reactivated"`
|
||||
TeamMembersReactivated int64 `json:"team_members_reactivated"`
|
||||
}
|
||||
|
|
@ -36,4 +36,6 @@ type TeamStore interface {
|
|||
CreateTeamRefreshToken(ctx context.Context, memberID int64, token string, expiresAt, createdAt time.Time) error
|
||||
GetTeamRefreshTokenByToken(ctx context.Context, token string) (domain.TeamRefreshToken, error)
|
||||
RevokeTeamRefreshTokenByToken(ctx context.Context, token string) error
|
||||
BulkDeactivateTeamMembersByRole(ctx context.Context, teamRole string, excludeTeamMemberID *int64) (int64, error)
|
||||
BulkReactivateTeamMembersByRole(ctx context.Context, teamRole string, excludeTeamMemberID *int64) (int64, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ type UserStore interface {
|
|||
GetUserDeviceTokens(ctx context.Context, userID int64) ([]string, error)
|
||||
DeactivateDevice(ctx context.Context, userID int64, deviceToken string) error
|
||||
DeactivateAllUserDevices(ctx context.Context, userID int64) error
|
||||
BulkDeactivateUsersByRole(ctx context.Context, role string, excludeUserID int64) (int64, error)
|
||||
BulkReactivateUsersByRole(ctx context.Context, role string, excludeUserID int64) (int64, error)
|
||||
}
|
||||
type SmsGateway interface {
|
||||
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
||||
|
|
|
|||
|
|
@ -219,6 +219,28 @@ func (s *Store) DeleteTeamMember(ctx context.Context, memberID int64) error {
|
|||
return s.queries.DeleteTeamMember(ctx, memberID)
|
||||
}
|
||||
|
||||
func (s *Store) BulkDeactivateTeamMembersByRole(ctx context.Context, teamRole string, excludeTeamMemberID *int64) (int64, error) {
|
||||
var ex pgtype.Int8
|
||||
if excludeTeamMemberID != nil {
|
||||
ex = pgtype.Int8{Int64: *excludeTeamMemberID, Valid: true}
|
||||
}
|
||||
return s.queries.BulkDeactivateTeamMembersByRole(ctx, dbgen.BulkDeactivateTeamMembersByRoleParams{
|
||||
TeamRole: teamRole,
|
||||
ExcludeTeamMemberID: ex,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) BulkReactivateTeamMembersByRole(ctx context.Context, teamRole string, excludeTeamMemberID *int64) (int64, error) {
|
||||
var ex pgtype.Int8
|
||||
if excludeTeamMemberID != nil {
|
||||
ex = pgtype.Int8{Int64: *excludeTeamMemberID, Valid: true}
|
||||
}
|
||||
return s.queries.BulkReactivateTeamMembersByRole(ctx, dbgen.BulkReactivateTeamMembersByRoleParams{
|
||||
TeamRole: teamRole,
|
||||
ExcludeTeamMemberID: ex,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) CheckTeamMemberEmailExists(ctx context.Context, email string) (bool, error) {
|
||||
return s.queries.CheckTeamMemberEmailExists(ctx, email)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,20 @@ func (s *Store) UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatu
|
|||
})
|
||||
}
|
||||
|
||||
func (s *Store) BulkDeactivateUsersByRole(ctx context.Context, role string, excludeUserID int64) (int64, error) {
|
||||
return s.queries.BulkDeactivateUsersByRole(ctx, dbgen.BulkDeactivateUsersByRoleParams{
|
||||
Role: role,
|
||||
ID: excludeUserID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) BulkReactivateUsersByRole(ctx context.Context, role string, excludeUserID int64) (int64, error) {
|
||||
return s.queries.BulkReactivateUsersByRole(ctx, dbgen.BulkReactivateUsersByRoleParams{
|
||||
Role: role,
|
||||
ID: excludeUserID,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) CreateUserWithoutOtp(
|
||||
ctx context.Context,
|
||||
user domain.User,
|
||||
|
|
|
|||
|
|
@ -119,6 +119,14 @@ func (s *Service) GetTeamMemberStats(ctx context.Context) (domain.TeamMemberStat
|
|||
return s.teamStore.CountTeamMembersByStatus(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) BulkDeactivateTeamMembersByRole(ctx context.Context, teamRole string, excludeTeamMemberID *int64) (int64, error) {
|
||||
return s.teamStore.BulkDeactivateTeamMembersByRole(ctx, teamRole, excludeTeamMemberID)
|
||||
}
|
||||
|
||||
func (s *Service) BulkReactivateTeamMembersByRole(ctx context.Context, teamRole string, excludeTeamMemberID *int64) (int64, error) {
|
||||
return s.teamStore.BulkReactivateTeamMembersByRole(ctx, teamRole, excludeTeamMemberID)
|
||||
}
|
||||
|
||||
func (s *Service) Login(ctx context.Context, req domain.TeamMemberLoginReq) (domain.TeamMember, error) {
|
||||
member, err := s.teamStore.GetTeamMemberByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -133,6 +133,14 @@ func (s *Service) UpdateUserStatus(ctx context.Context, req domain.UpdateUserSta
|
|||
return s.userStore.UpdateUserStatus(ctx, req)
|
||||
}
|
||||
|
||||
func (s *Service) BulkDeactivateUsersByRole(ctx context.Context, role string, excludeUserID int64) (int64, error) {
|
||||
return s.userStore.BulkDeactivateUsersByRole(ctx, role, excludeUserID)
|
||||
}
|
||||
|
||||
func (s *Service) BulkReactivateUsersByRole(ctx context.Context, role string, excludeUserID int64) (int64, error) {
|
||||
return s.userStore.BulkReactivateUsersByRole(ctx, role, excludeUserID)
|
||||
}
|
||||
|
||||
func (s *Service) GetUserById(ctx context.Context, id int64) (domain.User, error) {
|
||||
|
||||
return s.userStore.GetUserByID(ctx, id)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
|
@ -379,3 +380,247 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
|
|||
|
||||
return response.WriteJSON(c, fiber.StatusOK, "Admin updated successfully", nil, nil)
|
||||
}
|
||||
|
||||
// BulkDeactivateAccountsByRole godoc
|
||||
// @Summary Bulk deactivate accounts by role (SUPER_ADMIN 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. 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 body body domain.BulkAccountsByRoleRequest false "Optional exclusions"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 403 {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 {
|
||||
callerRole, ok := c.Locals("role").(domain.Role)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: "Forbidden",
|
||||
Error: "role not found in context",
|
||||
})
|
||||
}
|
||||
if callerRole != domain.RoleSuperAdmin {
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: "Forbidden",
|
||||
Error: "only SUPER_ADMIN platform users may bulk deactivate by role",
|
||||
})
|
||||
}
|
||||
|
||||
actorID, ok := c.Locals("user_id").(int64)
|
||||
if !ok || actorID <= 0 {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
||||
Message: "Unauthorized",
|
||||
Error: "user id not found in context",
|
||||
})
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
}
|
||||
if roleKey == string(domain.RoleSuperAdmin) || roleKey == string(domain.TeamRoleSuperAdmin) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Refusing bulk deactivate",
|
||||
Error: "SUPER_ADMIN cannot be bulk deactivated",
|
||||
})
|
||||
}
|
||||
|
||||
validUserRole := domain.Role(roleKey).IsValid()
|
||||
validTeamRole := domain.TeamRole(roleKey).IsValid()
|
||||
if !validUserRole && !validTeamRole {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid role",
|
||||
Error: "role is not a valid platform users.role nor team_members.team_role",
|
||||
})
|
||||
}
|
||||
|
||||
var req domain.BulkAccountsByRoleRequest
|
||||
if len(c.Body()) > 0 {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
if req.ExcludeTeamMemberID != nil && *req.ExcludeTeamMemberID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid exclude_team_member_id",
|
||||
Error: "exclude_team_member_id must be positive when set",
|
||||
})
|
||||
}
|
||||
|
||||
var usersN, teamN int64
|
||||
var err error
|
||||
if validUserRole {
|
||||
usersN, err = h.userSvc.BulkDeactivateUsersByRole(c.Context(), roleKey, actorID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Bulk user deactivation failed",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
if validTeamRole {
|
||||
teamN, err = h.teamSvc.BulkDeactivateTeamMembersByRole(c.Context(), roleKey, req.ExcludeTeamMemberID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Bulk team member deactivation failed",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
out := domain.BulkDeactivateAccountsByRoleResult{
|
||||
Role: roleKey,
|
||||
UsersDeactivated: usersN,
|
||||
TeamMembersDeactivated: teamN,
|
||||
}
|
||||
|
||||
actorRole := string(callerRole)
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{
|
||||
"role": roleKey,
|
||||
"users_deactivated": usersN,
|
||||
"team_members_deactivated": teamN,
|
||||
"exclude_team_member_id": req.ExcludeTeamMemberID,
|
||||
})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionUserUpdated, domain.ResourceUser, &actorID, fmt.Sprintf("Bulk deactivated role %s (%d users, %d team members)", roleKey, usersN, teamN), meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Bulk deactivation completed",
|
||||
Data: out,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// BulkReactivateAccountsByRole godoc
|
||||
// @Summary Bulk reactivate accounts by role (SUPER_ADMIN 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. 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 body body domain.BulkAccountsByRoleRequest false "Optional exclusions"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 403 {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 {
|
||||
callerRole, ok := c.Locals("role").(domain.Role)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: "Forbidden",
|
||||
Error: "role not found in context",
|
||||
})
|
||||
}
|
||||
if callerRole != domain.RoleSuperAdmin {
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: "Forbidden",
|
||||
Error: "only SUPER_ADMIN platform users may bulk reactivate by role",
|
||||
})
|
||||
}
|
||||
|
||||
actorID, ok := c.Locals("user_id").(int64)
|
||||
if !ok || actorID <= 0 {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
||||
Message: "Unauthorized",
|
||||
Error: "user id not found in context",
|
||||
})
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
}
|
||||
if roleKey == string(domain.RoleSuperAdmin) || roleKey == string(domain.TeamRoleSuperAdmin) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Refusing bulk reactivate",
|
||||
Error: "SUPER_ADMIN role cannot be bulk reactivated via this endpoint",
|
||||
})
|
||||
}
|
||||
|
||||
validUserRole := domain.Role(roleKey).IsValid()
|
||||
validTeamRole := domain.TeamRole(roleKey).IsValid()
|
||||
if !validUserRole && !validTeamRole {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid role",
|
||||
Error: "role is not a valid platform users.role nor team_members.team_role",
|
||||
})
|
||||
}
|
||||
|
||||
var req domain.BulkAccountsByRoleRequest
|
||||
if len(c.Body()) > 0 {
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
if req.ExcludeTeamMemberID != nil && *req.ExcludeTeamMemberID <= 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid exclude_team_member_id",
|
||||
Error: "exclude_team_member_id must be positive when set",
|
||||
})
|
||||
}
|
||||
|
||||
var usersN, teamN int64
|
||||
var err error
|
||||
if validUserRole {
|
||||
usersN, err = h.userSvc.BulkReactivateUsersByRole(c.Context(), roleKey, actorID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Bulk user reactivation failed",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
if validTeamRole {
|
||||
teamN, err = h.teamSvc.BulkReactivateTeamMembersByRole(c.Context(), roleKey, req.ExcludeTeamMemberID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Bulk team member reactivation failed",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
out := domain.BulkReactivateAccountsByRoleResult{
|
||||
Role: roleKey,
|
||||
UsersReactivated: usersN,
|
||||
TeamMembersReactivated: teamN,
|
||||
}
|
||||
|
||||
actorRoleStr := string(callerRole)
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
meta, _ := json.Marshal(map[string]interface{}{
|
||||
"role": roleKey,
|
||||
"users_reactivated": usersN,
|
||||
"team_members_reactivated": teamN,
|
||||
"exclude_team_member_id": req.ExcludeTeamMemberID,
|
||||
})
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRoleStr, domain.ActionUserUpdated, domain.ResourceUser, &actorID, fmt.Sprintf("Bulk reactivated role %s (%d users, %d team members)", roleKey, usersN, teamN), meta, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Bulk reactivation completed",
|
||||
Data: out,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -305,6 +305,8 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Get("/admin/:id", a.authMiddleware, a.RequirePermission("admins.get"), h.GetAdminByID)
|
||||
groupV1.Post("/admin", a.authMiddleware, a.RequirePermission("admins.create"), h.CreateAdmin)
|
||||
groupV1.Put("/admin/:id", a.authMiddleware, a.RequirePermission("admins.update"), h.UpdateAdmin)
|
||||
groupV1.Post("/admin/roles/:role/bulk-deactivate", a.authMiddleware, h.BulkDeactivateAccountsByRole)
|
||||
groupV1.Post("/admin/roles/:role/bulk-reactivate", a.authMiddleware, h.BulkReactivateAccountsByRole)
|
||||
|
||||
// Logs
|
||||
groupV1.Get("/logs", a.authMiddleware, a.RequirePermission("logs.list"), handlers.GetLogsHandler(context.Background()))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user