四端上传
This commit is contained in:
1 parent
25bbb5f9c8
commit
c6e2cb4d5b
276 files changed
+17812
-3
No files matched your search
@@ -0,0 +1,172 @@
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Put, Query } from '@nestjs/common';
|
||||
import { z } from 'zod';
|
||||
import { CurrentUser, Public, RequiresAudience, type AuthPrincipal } from '../../common/auth';
|
||||
import { AppError } from '../../common/errors';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { AdminService } from './admin.service';
|
||||
|
||||
const loginSchema = z.object({ username: z.string().min(1), password: z.string().min(1) });
|
||||
const bootstrapSchema = z.object({ username: z.string().min(3), password: z.string().min(8) });
|
||||
const updateUserSchema = z
|
||||
.object({
|
||||
nickname: z.string().max(32).optional(),
|
||||
headicon_id: z.coerce.number().int().min(0).optional(),
|
||||
status: z.coerce.number().int().min(0).max(1).optional(),
|
||||
})
|
||||
.strict();
|
||||
const settingSchema = z.object({ key: z.string().min(1).max(64), value: z.string(), remark: z.string().optional() });
|
||||
const grantSchema = z.object({
|
||||
userId: z.coerce.number().int().positive(),
|
||||
courseId: z.coerce.number().int().positive(),
|
||||
expireAt: z.coerce.date().nullish(),
|
||||
note: z.string().max(255).optional(),
|
||||
});
|
||||
const refundSchema = z.object({ reason: z.string().min(1).max(100) });
|
||||
|
||||
/** 管理员登录。必须公开,否则登录接口本身会被守卫拦住。 */
|
||||
@Public()
|
||||
@Controller('admin/v1/auth')
|
||||
export class AdminAuthController {
|
||||
constructor(
|
||||
private readonly admin: AdminService,
|
||||
private readonly auth: AuthService,
|
||||
) {}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() body: unknown) {
|
||||
const parsed = loginSchema.safeParse(body);
|
||||
if (!parsed.success) throw AppError.badRequest('请输入账号与密码');
|
||||
const admin = await this.admin.verifyAdmin(parsed.data.username, parsed.data.password);
|
||||
return { accessToken: await this.auth.signAdminToken(admin.id), admin };
|
||||
}
|
||||
|
||||
/**
|
||||
* 首次部署创建超管。仅当系统中尚无任何管理员时生效,
|
||||
* 因此可以安全地保留在接口里作为初始化入口。
|
||||
*/
|
||||
@Post('bootstrap')
|
||||
bootstrap(@Body() body: unknown) {
|
||||
const parsed = bootstrapSchema.safeParse(body);
|
||||
if (!parsed.success) throw AppError.badRequest('用户名至少 3 位,密码至少 8 位');
|
||||
return this.admin.bootstrapAdmin(parsed.data.username, parsed.data.password);
|
||||
}
|
||||
}
|
||||
|
||||
/** 管理后台主接口。全部要求管理员身份。 */
|
||||
@RequiresAudience('admin')
|
||||
@Controller('admin/v1')
|
||||
export class AdminController {
|
||||
constructor(private readonly admin: AdminService) {}
|
||||
|
||||
@Get('stats')
|
||||
stats() {
|
||||
return this.admin.stats();
|
||||
}
|
||||
|
||||
// ── 用户 ───────────────────────────────────────────────────────────────
|
||||
|
||||
@Get('users')
|
||||
listUsers(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
) {
|
||||
return this.admin.listUsers(Number(page) || 1, Number(pageSize) || 20, keyword ?? '');
|
||||
}
|
||||
|
||||
@Patch('users/:id')
|
||||
updateUser(@Param('id', ParseIntPipe) id: number, @Body() body: unknown) {
|
||||
const parsed = updateUserSchema.safeParse(body);
|
||||
if (!parsed.success) throw AppError.badRequest('参数错误');
|
||||
return this.admin.updateUser(id, parsed.data);
|
||||
}
|
||||
|
||||
// ── 通用内容 CRUD ──────────────────────────────────────────────────────
|
||||
|
||||
@Get('data/:resource')
|
||||
listResource(
|
||||
@Param('resource') resource: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('courseId') courseId?: string,
|
||||
) {
|
||||
const filter = courseId ? { course_id: Number(courseId) } : {};
|
||||
return this.admin.listResource(resource, Number(page) || 1, Number(pageSize) || 20, keyword ?? '', filter);
|
||||
}
|
||||
|
||||
@Post('data/:resource')
|
||||
createResource(@Param('resource') resource: string, @Body() body: unknown) {
|
||||
return this.admin.createResource(resource, body);
|
||||
}
|
||||
|
||||
@Patch('data/:resource/:id')
|
||||
updateResource(
|
||||
@Param('resource') resource: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() body: unknown,
|
||||
) {
|
||||
return this.admin.updateResource(resource, id, body);
|
||||
}
|
||||
|
||||
@Delete('data/:resource/:id')
|
||||
deleteResource(@Param('resource') resource: string, @Param('id', ParseIntPipe) id: number) {
|
||||
return this.admin.deleteResource(resource, id);
|
||||
}
|
||||
|
||||
// ── 配置 ───────────────────────────────────────────────────────────────
|
||||
|
||||
@Get('settings')
|
||||
listSettings() {
|
||||
return this.admin.listSettings();
|
||||
}
|
||||
|
||||
@Put('settings')
|
||||
upsertSetting(@Body() body: unknown) {
|
||||
const parsed = settingSchema.safeParse(body);
|
||||
if (!parsed.success) throw AppError.badRequest('参数错误');
|
||||
return this.admin.upsertSetting(parsed.data.key, parsed.data.value, parsed.data.remark);
|
||||
}
|
||||
|
||||
// ── 订单 / 权益 ────────────────────────────────────────────────────────
|
||||
|
||||
@Get('orders')
|
||||
listOrders(@Query('page') page?: string, @Query('pageSize') pageSize?: string, @Query('status') status?: string) {
|
||||
const s = status === undefined || status === '' ? undefined : Number(status);
|
||||
return this.admin.listOrders(Number(page) || 1, Number(pageSize) || 20, Number.isFinite(s) ? s : undefined);
|
||||
}
|
||||
|
||||
@Post('orders/:id/refund')
|
||||
refundOrder(@Param('id', ParseIntPipe) id: number, @Body() body: unknown) {
|
||||
const parsed = refundSchema.safeParse(body);
|
||||
if (!parsed.success) throw AppError.badRequest('请填写退款原因');
|
||||
return this.admin.refundOrder(id, parsed.data.reason);
|
||||
}
|
||||
|
||||
@Get('entitlements')
|
||||
listEntitlements(@Query('userId', ParseIntPipe) userId: number) {
|
||||
return this.admin.listEntitlements(userId);
|
||||
}
|
||||
|
||||
@Post('entitlements')
|
||||
grantEntitlement(@Body() body: unknown) {
|
||||
const parsed = grantSchema.safeParse(body);
|
||||
if (!parsed.success) throw AppError.badRequest('参数错误');
|
||||
return this.admin.grantEntitlement({
|
||||
userId: parsed.data.userId,
|
||||
courseId: parsed.data.courseId,
|
||||
expireAt: parsed.data.expireAt ?? null,
|
||||
note: parsed.data.note,
|
||||
});
|
||||
}
|
||||
|
||||
@Delete('entitlements')
|
||||
revokeEntitlement(@Query('userId', ParseIntPipe) userId: number, @Query('courseId', ParseIntPipe) courseId: number) {
|
||||
return this.admin.revokeEntitlement(userId, courseId);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
me(@CurrentUser() admin: AuthPrincipal) {
|
||||
return { id: admin.id, audience: admin.audience };
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user