feat(identity-service): 添加管理员登录功能
- 新增 AdminAccount 数据表存储管理员账户 - 在 AuthController 添加 POST /auth/login 端点 - 支持邮箱+密码登录,使用 bcrypt 验证 - 在 seed.ts 中初始化默认管理员账户 - 邮箱: admin@rwadurian.com - 密码: Admin@123456 - 前端登录页面适配新的响应格式 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f43124894d
commit
cb40463521
|
|
@ -0,0 +1,26 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "admin_accounts" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"email" VARCHAR(100) NOT NULL,
|
||||
"password_hash" VARCHAR(100) NOT NULL,
|
||||
"nickname" VARCHAR(100) NOT NULL,
|
||||
"role" VARCHAR(20) NOT NULL DEFAULT 'admin',
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"last_login_at" TIMESTAMP(3),
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "admin_accounts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "admin_accounts_email_key" ON "admin_accounts"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_admin_email" ON "admin_accounts"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_admin_role" ON "admin_accounts"("role");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_admin_status" ON "admin_accounts"("status");
|
||||
|
|
@ -307,6 +307,27 @@ model ReferralLink {
|
|||
@@map("referral_links")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 管理员账户表 - 用于后台管理系统登录
|
||||
// ============================================
|
||||
model AdminAccount {
|
||||
id BigInt @id @default(autoincrement())
|
||||
email String @unique @db.VarChar(100)
|
||||
passwordHash String @map("password_hash") @db.VarChar(100) // bcrypt 哈希
|
||||
nickname String @db.VarChar(100)
|
||||
role String @default("admin") @db.VarChar(20) // super_admin, admin, operator
|
||||
status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, INACTIVE, LOCKED
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
lastLoginAt DateTime? @map("last_login_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@index([email], name: "idx_admin_email")
|
||||
@@index([role], name: "idx_admin_role")
|
||||
@@index([status], name: "idx_admin_status")
|
||||
@@map("admin_accounts")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Outbox 事件表 - 保证事件可靠发送
|
||||
// 使用 Outbox Pattern 确保领域事件100%送达
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
|
@ -6,6 +7,19 @@ const prisma = new PrismaClient();
|
|||
// 系统账户定义
|
||||
// 系统账户使用特殊序列号格式: S + 00000 + 序号 (S0000000001 ~ S0000000004)
|
||||
// ============================================
|
||||
// ============================================
|
||||
// 管理员账户定义
|
||||
// 默认管理员密码: Admin@123456
|
||||
// ============================================
|
||||
const ADMIN_ACCOUNTS = [
|
||||
{
|
||||
email: 'admin@rwadurian.com',
|
||||
password: 'Admin@123456',
|
||||
nickname: '超级管理员',
|
||||
role: 'super_admin',
|
||||
},
|
||||
];
|
||||
|
||||
const SYSTEM_ACCOUNTS = [
|
||||
{
|
||||
userId: BigInt(1),
|
||||
|
|
@ -76,9 +90,32 @@ async function main() {
|
|||
console.log(` - Created system account: ${account.nickname} (accountSequence=${account.accountSequence})`);
|
||||
}
|
||||
|
||||
// 创建管理员账户
|
||||
console.log('Creating admin accounts...');
|
||||
for (const admin of ADMIN_ACCOUNTS) {
|
||||
const passwordHash = await bcrypt.hash(admin.password, 10);
|
||||
await prisma.adminAccount.upsert({
|
||||
where: { email: admin.email },
|
||||
update: {
|
||||
passwordHash,
|
||||
nickname: admin.nickname,
|
||||
role: admin.role,
|
||||
},
|
||||
create: {
|
||||
email: admin.email,
|
||||
passwordHash,
|
||||
nickname: admin.nickname,
|
||||
role: admin.role,
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
});
|
||||
console.log(` - Created admin account: ${admin.email} (role=${admin.role})`);
|
||||
}
|
||||
|
||||
console.log('Database seeded successfully!');
|
||||
console.log(`- Initialized account sequence generator for date ${dateKey}`);
|
||||
console.log(`- Created ${SYSTEM_ACCOUNTS.length} system accounts (S0000000001-S0000000004)`);
|
||||
console.log(`- Created ${ADMIN_ACCOUNTS.length} admin accounts`);
|
||||
}
|
||||
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
import { Controller, Post, Body } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { Controller, Post, Body, UnauthorizedException, Logger } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { PrismaService } from '@/infrastructure/persistence/prisma.service';
|
||||
import { UserApplicationService } from '@/application/services/user-application.service';
|
||||
import { Public } from '@/shared/guards/jwt-auth.guard';
|
||||
import { AutoLoginCommand } from '@/application/commands';
|
||||
import { AutoLoginDto } from '@/api/dto';
|
||||
import { AutoLoginDto, AdminLoginDto, AdminLoginResponseDto } from '@/api/dto';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
@ApiTags('Auth')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly userService: UserApplicationService) {}
|
||||
private readonly logger = new Logger(AuthController.name);
|
||||
|
||||
constructor(
|
||||
private readonly userService: UserApplicationService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly prisma: PrismaService,
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@Post('refresh')
|
||||
|
|
@ -16,4 +25,65 @@ export class AuthController {
|
|||
async refresh(@Body() dto: AutoLoginDto) {
|
||||
return this.userService.autoLogin(new AutoLoginCommand(dto.refreshToken, dto.deviceId));
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('login')
|
||||
@ApiOperation({ summary: '管理员登录 (邮箱+密码)' })
|
||||
@ApiResponse({ status: 200, type: AdminLoginResponseDto })
|
||||
@ApiResponse({ status: 401, description: '邮箱或密码错误' })
|
||||
async adminLogin(@Body() dto: AdminLoginDto): Promise<AdminLoginResponseDto> {
|
||||
this.logger.log(`[AdminLogin] 尝试登录: ${dto.email}`);
|
||||
|
||||
// 从数据库查找管理员
|
||||
const admin = await this.prisma.adminAccount.findUnique({
|
||||
where: { email: dto.email },
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
this.logger.warn(`[AdminLogin] 管理员不存在: ${dto.email}`);
|
||||
throw new UnauthorizedException('邮箱或密码错误');
|
||||
}
|
||||
|
||||
// 检查账户状态
|
||||
if (admin.status !== 'ACTIVE') {
|
||||
this.logger.warn(`[AdminLogin] 账户状态异常: ${dto.email}, status=${admin.status}`);
|
||||
throw new UnauthorizedException('账户已被禁用');
|
||||
}
|
||||
|
||||
// 验证密码 (使用 bcrypt)
|
||||
const isPasswordValid = await bcrypt.compare(dto.password, admin.passwordHash);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
this.logger.warn(`[AdminLogin] 密码错误: ${dto.email}`);
|
||||
throw new UnauthorizedException('邮箱或密码错误');
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
await this.prisma.adminAccount.update({
|
||||
where: { id: admin.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
|
||||
// 生成 JWT Token
|
||||
const payload = {
|
||||
sub: admin.id.toString(),
|
||||
email: admin.email,
|
||||
role: admin.role,
|
||||
type: 'admin',
|
||||
};
|
||||
|
||||
const accessToken = this.jwtService.sign(payload, { expiresIn: '24h' });
|
||||
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
|
||||
|
||||
this.logger.log(`[AdminLogin] 登录成功: ${dto.email}`);
|
||||
|
||||
return {
|
||||
userId: admin.id.toString(),
|
||||
email: admin.email,
|
||||
nickname: admin.nickname,
|
||||
role: admin.role,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,38 @@ export class LoginDto {
|
|||
deviceId: string;
|
||||
}
|
||||
|
||||
export class AdminLoginDto {
|
||||
@ApiProperty({ example: 'admin@example.com', description: '管理员邮箱' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '邮箱不能为空' })
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ example: 'password123', description: '密码' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '密码不能为空' })
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class AdminLoginResponseDto {
|
||||
@ApiProperty()
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ description: '管理员邮箱' })
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ description: '管理员昵称' })
|
||||
nickname: string;
|
||||
|
||||
@ApiProperty({ description: '角色' })
|
||||
role: string;
|
||||
|
||||
@ApiProperty()
|
||||
accessToken: string;
|
||||
|
||||
@ApiProperty()
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export class UpdateProfileDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
|
|
|
|||
|
|
@ -74,30 +74,31 @@ export default function LoginPage() {
|
|||
|
||||
console.log('[Login] 登录响应:', response);
|
||||
|
||||
// 根据后端返回的数据结构处理
|
||||
const { user, token, accessToken, permissions } = response.data || response;
|
||||
const finalToken = token || accessToken;
|
||||
// 后端返回格式: { userId, email, nickname, role, accessToken, refreshToken }
|
||||
const data = response.data || response;
|
||||
const { userId, email, nickname, role, accessToken, refreshToken } = data;
|
||||
|
||||
if (!finalToken) {
|
||||
console.error('[Login] 响应中没有 token:', response);
|
||||
if (!accessToken) {
|
||||
console.error('[Login] 响应中没有 accessToken:', response);
|
||||
throw new Error('登录响应缺少 token');
|
||||
}
|
||||
|
||||
dispatch(
|
||||
setCredentials({
|
||||
user: user || {
|
||||
id: '1',
|
||||
email: formData.email,
|
||||
username: formData.email.split('@')[0],
|
||||
nickname: '管理员',
|
||||
user: {
|
||||
id: userId,
|
||||
email: email,
|
||||
username: email.split('@')[0],
|
||||
nickname: nickname || '管理员',
|
||||
avatar: '',
|
||||
role: 'super_admin',
|
||||
role: role || 'super_admin',
|
||||
status: 'active',
|
||||
createdAt: new Date().toISOString(),
|
||||
lastLoginAt: new Date().toISOString(),
|
||||
},
|
||||
token: finalToken,
|
||||
permissions: permissions || ['*'],
|
||||
token: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
permissions: ['*'],
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue