feat: add admin authentication
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
"@nestjs/core": "^11.0.11",
|
||||
"@nestjs/platform-fastify": "^11.0.11",
|
||||
"@prisma/client": "^6.2.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"fastify": "^5.2.1",
|
||||
|
||||
@@ -138,6 +138,18 @@ model Tag {
|
||||
@@unique([brandId, name])
|
||||
}
|
||||
|
||||
model AdminUser {
|
||||
id String @id @default(uuid())
|
||||
username String @unique
|
||||
passwordHash String
|
||||
displayName String
|
||||
role String @default("admin")
|
||||
isEnabled Boolean @default(true)
|
||||
lastLoginAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model BookingTag {
|
||||
bookingId String
|
||||
tagId String
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { hash } from "bcryptjs";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@@ -54,6 +55,15 @@ async function main(): Promise<void> {
|
||||
create: { brandId: brand.id, name, isPreset: true },
|
||||
});
|
||||
}
|
||||
await prisma.adminUser.upsert({
|
||||
where: { username: "admin" },
|
||||
update: {},
|
||||
create: {
|
||||
username: "admin",
|
||||
passwordHash: await hash(process.env.SEED_ADMIN_PASSWORD ?? "change-me-now-123", 12),
|
||||
displayName: "品牌管理员",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
main().finally(() => prisma.$disconnect());
|
||||
|
||||
6
apps/api/src/admin-auth.dto.ts
Normal file
6
apps/api/src/admin-auth.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { IsString, MinLength } from "class-validator";
|
||||
|
||||
export class AdminLoginDto {
|
||||
@IsString() username!: string;
|
||||
@IsString() @MinLength(8) password!: string;
|
||||
}
|
||||
30
apps/api/src/admin-auth.service.ts
Normal file
30
apps/api/src/admin-auth.service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { compare } from "bcryptjs";
|
||||
import { PrismaService } from "./prisma.service.js";
|
||||
import { signIdentity } from "./auth.js";
|
||||
|
||||
@Injectable()
|
||||
export class AdminAuthService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async login(username: string, password: string): Promise<{ token: string; displayName: string }> {
|
||||
const user = await this.prisma.adminUser.findUnique({ where: { username } });
|
||||
// bcryptjs ships incomplete typings in this dependency version.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
if (!user || !user.isEnabled || !(await compare(password, user.passwordHash))) {
|
||||
throw new UnauthorizedException("账号或密码错误");
|
||||
}
|
||||
await this.prisma.adminUser.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
return {
|
||||
token: signIdentity({
|
||||
subject: user.id,
|
||||
brandId: process.env.DEFAULT_BRAND_ID ?? "",
|
||||
role: "admin",
|
||||
}),
|
||||
displayName: user.displayName,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import { WechatAuthService } from "./wechat-auth.js";
|
||||
import { BookingService } from "./booking.service.js";
|
||||
import { CreateBookingDto } from "./booking.dto.js";
|
||||
import { UpdateBookingStatusDto } from "./admin.dto.js";
|
||||
import { AdminAuthService } from "./admin-auth.service.js";
|
||||
import { AdminLoginDto } from "./admin-auth.dto.js";
|
||||
import type { FastifyRequest } from "fastify";
|
||||
import { readIdentity, signIdentity } from "./auth.js";
|
||||
|
||||
@@ -92,6 +94,15 @@ export class CustomerSessionController {
|
||||
}
|
||||
}
|
||||
|
||||
@Controller("admin/session")
|
||||
export class AdminSessionController {
|
||||
constructor(private readonly auth: AdminAuthService) {}
|
||||
@Post("login")
|
||||
login(@Body() body: AdminLoginDto): Promise<{ token: string; displayName: string }> {
|
||||
return this.auth.login(body.username, body.password);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller("customer/bookings")
|
||||
export class CustomerBookingController {
|
||||
constructor(
|
||||
@@ -185,10 +196,11 @@ export class AdminBookingController {
|
||||
HealthController,
|
||||
PublicController,
|
||||
CustomerSessionController,
|
||||
AdminSessionController,
|
||||
CustomerBookingController,
|
||||
AdminBookingController,
|
||||
],
|
||||
providers: [PrismaService, WechatAuthService, BookingService],
|
||||
providers: [PrismaService, WechatAuthService, BookingService, AdminAuthService],
|
||||
exports: [PrismaService, WechatAuthService],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
4
apps/api/src/types/bcryptjs.d.ts
vendored
Normal file
4
apps/api/src/types/bcryptjs.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "bcryptjs" {
|
||||
export function compare(password: string, hash: string): Promise<boolean>;
|
||||
export function hash(password: string, saltRounds: number): Promise<string>;
|
||||
}
|
||||
Reference in New Issue
Block a user