feat: add admin authentication
This commit is contained in:
@@ -4,6 +4,7 @@ API_PORT=3001
|
|||||||
ADMIN_PORT=3000
|
ADMIN_PORT=3000
|
||||||
SESSION_SECRET=replace-with-a-long-random-value
|
SESSION_SECRET=replace-with-a-long-random-value
|
||||||
DEFAULT_BRAND_ID=00000000-0000-0000-0000-000000000001
|
DEFAULT_BRAND_ID=00000000-0000-0000-0000-000000000001
|
||||||
|
SEED_ADMIN_PASSWORD=change-me-now-123
|
||||||
OBJECT_STORAGE_ENDPOINT=
|
OBJECT_STORAGE_ENDPOINT=
|
||||||
OBJECT_STORAGE_BUCKET=
|
OBJECT_STORAGE_BUCKET=
|
||||||
OBJECT_STORAGE_ACCESS_KEY=
|
OBJECT_STORAGE_ACCESS_KEY=
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@nestjs/core": "^11.0.11",
|
"@nestjs/core": "^11.0.11",
|
||||||
"@nestjs/platform-fastify": "^11.0.11",
|
"@nestjs/platform-fastify": "^11.0.11",
|
||||||
"@prisma/client": "^6.2.1",
|
"@prisma/client": "^6.2.1",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"fastify": "^5.2.1",
|
"fastify": "^5.2.1",
|
||||||
|
|||||||
@@ -138,6 +138,18 @@ model Tag {
|
|||||||
@@unique([brandId, name])
|
@@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 {
|
model BookingTag {
|
||||||
bookingId String
|
bookingId String
|
||||||
tagId String
|
tagId String
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { hash } from "bcryptjs";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
@@ -54,6 +55,15 @@ async function main(): Promise<void> {
|
|||||||
create: { brandId: brand.id, name, isPreset: true },
|
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());
|
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 { BookingService } from "./booking.service.js";
|
||||||
import { CreateBookingDto } from "./booking.dto.js";
|
import { CreateBookingDto } from "./booking.dto.js";
|
||||||
import { UpdateBookingStatusDto } from "./admin.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 type { FastifyRequest } from "fastify";
|
||||||
import { readIdentity, signIdentity } from "./auth.js";
|
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")
|
@Controller("customer/bookings")
|
||||||
export class CustomerBookingController {
|
export class CustomerBookingController {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -185,10 +196,11 @@ export class AdminBookingController {
|
|||||||
HealthController,
|
HealthController,
|
||||||
PublicController,
|
PublicController,
|
||||||
CustomerSessionController,
|
CustomerSessionController,
|
||||||
|
AdminSessionController,
|
||||||
CustomerBookingController,
|
CustomerBookingController,
|
||||||
AdminBookingController,
|
AdminBookingController,
|
||||||
],
|
],
|
||||||
providers: [PrismaService, WechatAuthService, BookingService],
|
providers: [PrismaService, WechatAuthService, BookingService, AdminAuthService],
|
||||||
exports: [PrismaService, WechatAuthService],
|
exports: [PrismaService, WechatAuthService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
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>;
|
||||||
|
}
|
||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -46,6 +46,9 @@ importers:
|
|||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^6.2.1
|
specifier: ^6.2.1
|
||||||
version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)
|
version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)
|
||||||
|
bcryptjs:
|
||||||
|
specifier: ^2.4.3
|
||||||
|
version: 2.4.3
|
||||||
class-transformer:
|
class-transformer:
|
||||||
specifier: ^0.5.1
|
specifier: ^0.5.1
|
||||||
version: 0.5.1
|
version: 0.5.1
|
||||||
@@ -572,6 +575,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
|
bcryptjs@2.4.3:
|
||||||
|
resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==}
|
||||||
|
|
||||||
brace-expansion@1.1.21:
|
brace-expansion@1.1.21:
|
||||||
resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==}
|
resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==}
|
||||||
|
|
||||||
@@ -1666,6 +1672,8 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@4.0.4: {}
|
balanced-match@4.0.4: {}
|
||||||
|
|
||||||
|
bcryptjs@2.4.3: {}
|
||||||
|
|
||||||
brace-expansion@1.1.21:
|
brace-expansion@1.1.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
|
|||||||
Reference in New Issue
Block a user