Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions pages/api/v2/admin/users/[userId]/role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { timingSafeEqual } from 'crypto'
import { ZodIssue, z } from 'zod'
import { ENV } from 'server/ENV'
import { Provider } from 'server/provider'
import { UserService } from 'server/service/user.service'
import { UserNotFoundError } from 'server/domain/error'
import {
AdminUserRoleUpdateParamsSchema,
AdminUserRoleUpdateSchema,
type AdminUserRoleUpdateResponse,
} from 'src/lib/schemas/admin'

const isValidAdminRoleApiKey = (apiKey: string | string[] | undefined): boolean => {
const expectedApiKey = ENV.APP_VERSION_POLICY.API_KEY
if (!expectedApiKey || !apiKey || Array.isArray(apiKey)) {
return false
}

const expectedBuffer = new TextEncoder().encode(expectedApiKey)
const actualBuffer = new TextEncoder().encode(apiKey)
return (
expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer)
)
}

export default async function handler(
req: NextApiRequest,
res: NextApiResponse<AdminUserRoleUpdateResponse | string | ZodIssue[]>,
) {
try {
if (!isValidAdminRoleApiKey(req.headers['x-internal-api-key'])) {
return res.status(401).send('Unauthorized')
}

if (req.method === 'PATCH') {
const { userId } = AdminUserRoleUpdateParamsSchema.parse(req.query)
const body = AdminUserRoleUpdateSchema.parse(req.body)

const userService = Provider.getService(UserService)
await userService.updateUserRole(userId, body.role)

return res.status(200).json({
success: true,
message: '사용자 권한이 변경되었습니다.',
data: {
user_id: userId,
role: body.role,
},
})
}
} catch (err) {
if (err instanceof UserNotFoundError) {
return res.status(404).send('User not found')
}
if (err instanceof z.ZodError) {
return res.status(400).json(err.errors)
}
console.error('updateUserRole error: ', err)
return res.status(500).send('Internal Server Error')
}

return res.status(405).end()
}
1 change: 1 addition & 0 deletions server/ENV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const ENV = {
APP_VERSION_POLICY: {
API_KEY: process.env.APP_VERSION_POLICY_API_KEY ?? '',
},

R2: {
ACCOUNT_ID: process.env.R2_ACCOUNT_ID ?? '',
ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID ?? '',
Expand Down
8 changes: 8 additions & 0 deletions server/service/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ export class UserService {
}
}

public async updateUserRole(userId: string, role: UserRole): Promise<void> {
const user = await this.userRepository.findOneBy({ id: userId })
if (!user) {
throw new UserNotFoundError(`User not found`)
}
await this.userRepository.update(userId, { role })
}

public async assertAdminRole(accountId: string): Promise<void> {
const accountUser = await this.accountUserRepository.findOne({
where: { accountId },
Expand Down
55 changes: 55 additions & 0 deletions src/lib/openapi/register-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ import {
AdminClubsResponseSchema,
AdminClubStatusUpdateResponseSchema,
AdminClubStatusUpdateSchema,
AdminUserRoleUpdateParamsSchema,
AdminUserRoleUpdateResponseSchema,
AdminUserRoleUpdateSchema,
} from 'src/lib/schemas/admin'
import {
CollegeMajorsQuerySchema,
Expand Down Expand Up @@ -2213,6 +2216,58 @@ registry.registerPath({
},
})

registry.registerPath({
method: 'patch',
path: '/api/v2/admin/users/{userId}/role',
tags: ['Admin'],
summary: '사용자 권한 변경',
description:
'내부 API 키 인증을 통해 특정 사용자의 권한을 admin 또는 user로 변경합니다. x-internal-api-key 헤더에 발급된 키를 포함해야 합니다.',
security: [{ internalApiKeyAuth: [] }],
request: {
params: AdminUserRoleUpdateParamsSchema,
body: {
content: {
'application/json': {
schema: AdminUserRoleUpdateSchema,
examples: {
grant_admin: {
summary: 'admin 권한 부여',
value: { role: 'admin' },
},
revoke_admin: {
summary: 'user로 권한 변경',
value: { role: 'user' },
},
},
},
},
},
},
responses: {
200: {
description: '권한 변경 성공',
content: {
'application/json': {
schema: AdminUserRoleUpdateResponseSchema,
example: {
success: true,
message: '사용자 권한이 변경되었습니다.',
data: {
user_id: '123e4567-e89b-12d3-a456-426614174000',
role: 'admin',
},
},
},
},
},
400: validationErrorResponse,
401: unauthorizedResponse,
404: notFoundResponse,
500: internalServerErrorResponse,
},
})

export const registeredClubRankingSchema = ClubRankingSchema
export const registeredMyReviewSchema = MyReviewSchema
export const registeredUserSchema = UserSchema
30 changes: 30 additions & 0 deletions src/lib/schemas/admin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'src/lib/schemas/zod'
import { CLUB_STATUSES, REJECTED_CLUB_STATUS } from 'src/common/constants/club-status'
import { UserRole } from 'server/infra/database/entities/user-role.enum'

const AdminClubManagerSchema = z.object({
name: z.string(),
Expand Down Expand Up @@ -328,3 +329,32 @@ export const AdminClubVerificationRequestStatusUpdateResponseSchema = z
export type AdminClubVerificationRequestStatusUpdateResponse = z.infer<
typeof AdminClubVerificationRequestStatusUpdateResponseSchema
>

export const AdminUserRoleUpdateParamsSchema = z
.object({
userId: z.string().uuid(),
})
.openapi('AdminUserRoleUpdateParams')

export type AdminUserRoleUpdateParams = z.infer<typeof AdminUserRoleUpdateParamsSchema>

export const AdminUserRoleUpdateSchema = z
.object({
role: z.enum([UserRole.ADMIN, UserRole.USER]),
})
.openapi('AdminUserRoleUpdate')

export type AdminUserRoleUpdate = z.infer<typeof AdminUserRoleUpdateSchema>

export const AdminUserRoleUpdateResponseSchema = z
.object({
success: z.literal(true),
message: z.string(),
data: z.object({
user_id: z.string().uuid(),
role: z.string(),
}),
})
.openapi('AdminUserRoleUpdateResponse')

export type AdminUserRoleUpdateResponse = z.infer<typeof AdminUserRoleUpdateResponseSchema>
Loading