feat: 初始化 WWJ Cloud 企业级框架项目
- 后端:基于 NestJS 的分层架构设计 - 前端:基于 VbenAdmin + Element Plus 的管理系统 - 支持 SaaS + 独立版双架构模式 - 完整的用户权限管理系统 - 系统设置、文件上传、通知等核心功能 - 多租户支持和插件化扩展架构
This commit is contained in:
145
wwjcloud/src/common/auth/auth.controller.ts
Normal file
145
wwjcloud/src/common/auth/auth.controller.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
UseGuards,
|
||||
Request,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LoginDto, RegisterDto, ChangePasswordDto, ResetPasswordDto } from './dto';
|
||||
import { LocalAuthGuard } from './guards/local-auth.guard';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { Public, CurrentUser, CurrentUserId } from './decorators/auth.decorator';
|
||||
|
||||
@ApiTags('认证授权')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Public()
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@Post('login')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '用户登录' })
|
||||
@ApiBody({ type: LoginDto })
|
||||
async login(@Request() req, @Body() loginDto: LoginDto) {
|
||||
const result = await this.authService.login(loginDto);
|
||||
return {
|
||||
code: 200,
|
||||
message: '登录成功',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('register')
|
||||
@ApiOperation({ summary: '会员注册' })
|
||||
async register(@Body() registerDto: RegisterDto) {
|
||||
const result = await this.authService.register(registerDto);
|
||||
return {
|
||||
code: 200,
|
||||
message: '注册成功',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('refresh')
|
||||
@ApiOperation({ summary: '刷新token' })
|
||||
async refreshToken(@Body('refreshToken') refreshToken: string) {
|
||||
const result = await this.authService.refreshToken(refreshToken);
|
||||
return {
|
||||
code: 200,
|
||||
message: '刷新成功',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('change-password')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: '修改密码' })
|
||||
async changePassword(
|
||||
@CurrentUserId() userId: number,
|
||||
@Body() changePasswordDto: ChangePasswordDto,
|
||||
) {
|
||||
const result = await this.authService.changePassword(userId, changePasswordDto);
|
||||
return {
|
||||
code: 200,
|
||||
message: '密码修改成功',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('reset-password')
|
||||
@ApiOperation({ summary: '重置密码' })
|
||||
async resetPassword(@Body() resetPasswordDto: ResetPasswordDto) {
|
||||
const result = await this.authService.resetPassword(resetPasswordDto);
|
||||
return {
|
||||
code: 200,
|
||||
message: '密码重置成功',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('logout')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: '用户登出' })
|
||||
async logout(@Request() req) {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
const result = await this.authService.logout(token);
|
||||
return {
|
||||
code: 200,
|
||||
message: '登出成功',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('profile')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: '获取当前用户信息' })
|
||||
async getProfile(@CurrentUser() user: any) {
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
userType: user.userType,
|
||||
siteId: user.siteId,
|
||||
nickname: user.user.nickname || user.user.realname,
|
||||
avatar: user.user.avatar,
|
||||
mobile: user.user.mobile,
|
||||
email: user.user.email,
|
||||
status: user.user.status,
|
||||
createTime: user.user.createTime,
|
||||
lastLoginTime: user.user.lastLoginTime,
|
||||
lastLoginIp: user.user.lastLoginIp,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('check')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: '检查token有效性' })
|
||||
async checkToken(@CurrentUser() user: any) {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Token有效',
|
||||
data: {
|
||||
valid: true,
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
userType: user.userType,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
58
wwjcloud/src/common/auth/auth.module.ts
Normal file
58
wwjcloud/src/common/auth/auth.module.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { UserPermissionController } from './user-permission.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { PermissionService } from './services/permission.service';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { LocalStrategy } from './strategies/local.strategy';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { LocalAuthGuard } from './guards/local-auth.guard';
|
||||
import { RolesGuard } from './guards/roles.guard';
|
||||
import { GlobalAuthGuard } from './guards/global-auth.guard';
|
||||
import { MemberModule } from '../member/member.module';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { RbacModule } from '../rbac/rbac.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET', 'wwjcloud-secret-key'),
|
||||
signOptions: {
|
||||
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1h'),
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
MemberModule,
|
||||
AdminModule,
|
||||
RbacModule,
|
||||
],
|
||||
controllers: [AuthController, UserPermissionController],
|
||||
providers: [
|
||||
AuthService,
|
||||
PermissionService,
|
||||
JwtStrategy,
|
||||
LocalStrategy,
|
||||
JwtAuthGuard,
|
||||
LocalAuthGuard,
|
||||
RolesGuard,
|
||||
GlobalAuthGuard,
|
||||
],
|
||||
exports: [
|
||||
AuthService,
|
||||
PermissionService,
|
||||
JwtAuthGuard,
|
||||
LocalAuthGuard,
|
||||
RolesGuard,
|
||||
GlobalAuthGuard,
|
||||
JwtModule,
|
||||
PassportModule,
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
318
wwjcloud/src/common/auth/auth.service.ts
Normal file
318
wwjcloud/src/common/auth/auth.service.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import { Injectable, UnauthorizedException, BadRequestException, ConflictException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { MemberService } from '../member/member.service';
|
||||
import { AdminService } from '../admin/admin.service';
|
||||
import { LoginDto, RegisterDto, ChangePasswordDto, ResetPasswordDto } from './dto';
|
||||
import { JwtPayload } from './strategies/jwt.strategy';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly memberService: MemberService,
|
||||
private readonly adminService: AdminService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 验证用户凭据
|
||||
*/
|
||||
async validateUser(username: string, password: string, userType: 'member' | 'admin' = 'member') {
|
||||
let user;
|
||||
|
||||
try {
|
||||
if (userType === 'member') {
|
||||
// 尝试通过用户名或手机号查找会员
|
||||
user = await this.memberService.findByUsername(username) ||
|
||||
await this.memberService.findByMobile(username);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await this.memberService.validatePassword(user.memberId, password);
|
||||
if (!isPasswordValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查账户状态
|
||||
if (user.status !== 1) {
|
||||
throw new UnauthorizedException('账户已被禁用');
|
||||
}
|
||||
|
||||
return {
|
||||
userId: user.memberId,
|
||||
username: user.username,
|
||||
userType: 'member',
|
||||
siteId: user.siteId,
|
||||
user,
|
||||
};
|
||||
} else if (userType === 'admin') {
|
||||
// 查找管理员
|
||||
user = await this.adminService.findByUsername(username);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查账户状态
|
||||
if (user.status !== 1) {
|
||||
throw new UnauthorizedException('账户已被禁用');
|
||||
}
|
||||
|
||||
return {
|
||||
userId: user.uid,
|
||||
username: user.username,
|
||||
userType: 'admin',
|
||||
siteId: user.siteId,
|
||||
user,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedException) {
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
async login(loginDto: LoginDto) {
|
||||
const { username, password, userType = 'member' } = loginDto;
|
||||
|
||||
const user = await this.validateUser(username, password, userType);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('用户名或密码错误');
|
||||
}
|
||||
|
||||
// 更新最后登录信息
|
||||
const loginInfo = {
|
||||
lastLoginTime: Math.floor(Date.now() / 1000),
|
||||
lastLoginIp: '127.0.0.1', // 实际项目中应该从请求中获取真实IP
|
||||
};
|
||||
|
||||
if (userType === 'member') {
|
||||
await this.memberService.updateLastLogin(user.userId, loginInfo);
|
||||
} else {
|
||||
await this.adminService.updateLastLogin(user.userId, loginInfo);
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
const payload: JwtPayload = {
|
||||
sub: user.userId,
|
||||
username: user.username,
|
||||
userType: user.userType,
|
||||
siteId: user.siteId,
|
||||
};
|
||||
|
||||
const accessToken = this.jwtService.sign(payload);
|
||||
const refreshToken = this.jwtService.sign(payload, {
|
||||
expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN', '7d'),
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: this.configService.get('JWT_EXPIRES_IN', '1h'),
|
||||
user: {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
userType: user.userType,
|
||||
siteId: user.siteId,
|
||||
nickname: user.user.nickname || user.user.realname,
|
||||
avatar: user.user.avatar,
|
||||
mobile: user.user.mobile,
|
||||
email: user.user.email,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员注册
|
||||
*/
|
||||
async register(registerDto: RegisterDto) {
|
||||
const { username, mobile, password, confirmPassword, ...otherData } = registerDto;
|
||||
|
||||
// 验证密码确认
|
||||
if (password !== confirmPassword) {
|
||||
throw new BadRequestException('两次输入的密码不一致');
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUserByUsername = await this.memberService.findByUsername(username);
|
||||
if (existingUserByUsername) {
|
||||
throw new ConflictException('用户名已存在');
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
const existingUserByMobile = await this.memberService.findByMobile(mobile);
|
||||
if (existingUserByMobile) {
|
||||
throw new ConflictException('手机号已被注册');
|
||||
}
|
||||
|
||||
// 创建会员
|
||||
const member = await this.memberService.create({
|
||||
username,
|
||||
mobile,
|
||||
password,
|
||||
nickname: otherData.nickname || username,
|
||||
email: otherData.email,
|
||||
siteId: otherData.siteId || 0,
|
||||
pid: otherData.pid || 0,
|
||||
sex: otherData.sex || 0,
|
||||
regType: otherData.regType || 'mobile',
|
||||
status: 1,
|
||||
});
|
||||
|
||||
return {
|
||||
userId: member.memberId,
|
||||
username: member.username,
|
||||
mobile: member.mobile,
|
||||
nickname: member.nickname,
|
||||
message: '注册成功',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
*/
|
||||
async refreshToken(refreshToken: string) {
|
||||
try {
|
||||
const payload = this.jwtService.verify(refreshToken);
|
||||
|
||||
// 验证用户是否仍然有效
|
||||
const user = await this.validateUser(payload.username, '', payload.userType);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('用户不存在或已被禁用');
|
||||
}
|
||||
|
||||
// 生成新的token
|
||||
const newPayload: JwtPayload = {
|
||||
sub: payload.sub,
|
||||
username: payload.username,
|
||||
userType: payload.userType,
|
||||
siteId: payload.siteId,
|
||||
};
|
||||
|
||||
const accessToken = this.jwtService.sign(newPayload);
|
||||
const newRefreshToken = this.jwtService.sign(newPayload, {
|
||||
expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN', '7d'),
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
tokenType: 'Bearer',
|
||||
expiresIn: this.configService.get('JWT_EXPIRES_IN', '1h'),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('刷新token失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
async changePassword(userId: number, changePasswordDto: ChangePasswordDto) {
|
||||
const { oldPassword, newPassword, confirmPassword, userType = 'member' } = changePasswordDto;
|
||||
|
||||
// 验证新密码确认
|
||||
if (newPassword !== confirmPassword) {
|
||||
throw new BadRequestException('两次输入的新密码不一致');
|
||||
}
|
||||
|
||||
let user;
|
||||
if (userType === 'member') {
|
||||
user = await this.memberService.findOne(userId);
|
||||
// 验证旧密码
|
||||
const isOldPasswordValid = await this.memberService.validatePassword(userId, oldPassword);
|
||||
if (!isOldPasswordValid) {
|
||||
throw new BadRequestException('旧密码错误');
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
await this.memberService.update(userId, { password: newPassword });
|
||||
} else {
|
||||
user = await this.adminService.findOne(userId);
|
||||
// 验证旧密码
|
||||
const isOldPasswordValid = await bcrypt.compare(oldPassword, user.password);
|
||||
if (!isOldPasswordValid) {
|
||||
throw new BadRequestException('旧密码错误');
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
await this.adminService.update(userId, { password: hashedPassword });
|
||||
}
|
||||
|
||||
return {
|
||||
message: '密码修改成功',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
async resetPassword(resetPasswordDto: ResetPasswordDto) {
|
||||
const { identifier, newPassword, confirmPassword, userType = 'member' } = resetPasswordDto;
|
||||
|
||||
// 验证新密码确认
|
||||
if (newPassword !== confirmPassword) {
|
||||
throw new BadRequestException('两次输入的新密码不一致');
|
||||
}
|
||||
|
||||
let user;
|
||||
if (userType === 'member') {
|
||||
// 通过用户名或手机号查找用户
|
||||
user = await this.memberService.findByUsername(identifier) ||
|
||||
await this.memberService.findByMobile(identifier);
|
||||
|
||||
if (!user) {
|
||||
throw new BadRequestException('用户不存在');
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
await this.memberService.update(user.memberId, { password: newPassword });
|
||||
} else {
|
||||
user = await this.adminService.findByUsername(identifier);
|
||||
|
||||
if (!user) {
|
||||
throw new BadRequestException('管理员不存在');
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
await this.adminService.update(user.uid, { password: hashedPassword });
|
||||
}
|
||||
|
||||
return {
|
||||
message: '密码重置成功',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出(可以在这里实现token黑名单等逻辑)
|
||||
*/
|
||||
async logout(token: string) {
|
||||
// 这里可以实现token黑名单逻辑
|
||||
// 目前简单返回成功消息
|
||||
return {
|
||||
message: '登出成功',
|
||||
};
|
||||
}
|
||||
}
|
||||
37
wwjcloud/src/common/auth/decorators/auth.decorator.ts
Normal file
37
wwjcloud/src/common/auth/decorators/auth.decorator.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
// 标记公开路由(不需要认证)
|
||||
export const Public = () => SetMetadata('isPublic', true);
|
||||
|
||||
// 设置所需角色
|
||||
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
|
||||
|
||||
// 设置所需权限
|
||||
export const Permissions = (...permissions: string[]) => SetMetadata('permissions', permissions);
|
||||
|
||||
// 获取当前用户信息
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: string, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
||||
|
||||
// 获取当前用户ID
|
||||
export const CurrentUserId = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user?.userId;
|
||||
},
|
||||
);
|
||||
|
||||
// 获取当前用户类型
|
||||
export const CurrentUserType = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user?.userType;
|
||||
},
|
||||
);
|
||||
66
wwjcloud/src/common/auth/dto/change-password.dto.ts
Normal file
66
wwjcloud/src/common/auth/dto/change-password.dto.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsIn } from 'class-validator';
|
||||
|
||||
export class ChangePasswordDto {
|
||||
@ApiProperty({ description: '旧密码' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '旧密码不能为空' })
|
||||
oldPassword: string;
|
||||
|
||||
@ApiProperty({ description: '新密码' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '新密码不能为空' })
|
||||
newPassword: string;
|
||||
|
||||
@ApiProperty({ description: '确认新密码' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '确认新密码不能为空' })
|
||||
confirmPassword: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '用户类型:member会员 admin管理员', default: 'member' })
|
||||
@IsOptional()
|
||||
@IsIn(['member', 'admin'])
|
||||
userType?: string = 'member';
|
||||
}
|
||||
|
||||
export class ResetPasswordDto {
|
||||
@ApiProperty({ description: '用户名/手机号/邮箱' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '用户标识不能为空' })
|
||||
identifier: string;
|
||||
|
||||
@ApiProperty({ description: '新密码' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '新密码不能为空' })
|
||||
newPassword: string;
|
||||
|
||||
@ApiProperty({ description: '确认新密码' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '确认新密码不能为空' })
|
||||
confirmPassword: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '用户类型:member会员 admin管理员', default: 'member' })
|
||||
@IsOptional()
|
||||
@IsIn(['member', 'admin'])
|
||||
userType?: string = 'member';
|
||||
|
||||
@ApiPropertyOptional({ description: '短信验证码' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
smsCode?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '邮箱验证码' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
emailCode?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '图形验证码' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
captcha?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '验证码key' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
captchaKey?: string;
|
||||
}
|
||||
3
wwjcloud/src/common/auth/dto/index.ts
Normal file
3
wwjcloud/src/common/auth/dto/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './login.dto';
|
||||
export * from './register.dto';
|
||||
export * from './change-password.dto';
|
||||
33
wwjcloud/src/common/auth/dto/login.dto.ts
Normal file
33
wwjcloud/src/common/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsIn } from 'class-validator';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({ description: '用户名/手机号' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '用户名不能为空' })
|
||||
username: string;
|
||||
|
||||
@ApiProperty({ description: '密码' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '密码不能为空' })
|
||||
password: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '用户类型:member会员 admin管理员', default: 'member' })
|
||||
@IsOptional()
|
||||
@IsIn(['member', 'admin'])
|
||||
userType?: string = 'member';
|
||||
|
||||
@ApiPropertyOptional({ description: '验证码' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
captcha?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '验证码key' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
captchaKey?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '记住我' })
|
||||
@IsOptional()
|
||||
rememberMe?: boolean;
|
||||
}
|
||||
79
wwjcloud/src/common/auth/dto/register.dto.ts
Normal file
79
wwjcloud/src/common/auth/dto/register.dto.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsNotEmpty, IsOptional, IsInt, IsIn, IsEmail, IsMobilePhone } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class RegisterDto {
|
||||
@ApiProperty({ description: '用户名' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '用户名不能为空' })
|
||||
username: string;
|
||||
|
||||
@ApiProperty({ description: '手机号' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '手机号不能为空' })
|
||||
@IsMobilePhone('zh-CN', {}, { message: '手机号格式不正确' })
|
||||
mobile: string;
|
||||
|
||||
@ApiProperty({ description: '密码' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '密码不能为空' })
|
||||
password: string;
|
||||
|
||||
@ApiProperty({ description: '确认密码' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '确认密码不能为空' })
|
||||
confirmPassword: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '昵称' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nickname?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '邮箱' })
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
email?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '站点ID' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
siteId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '推广会员ID' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
pid?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '性别:1男 2女 0未知', default: 0 })
|
||||
@IsOptional()
|
||||
@IsIn([0, 1, 2])
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
sex?: number = 0;
|
||||
|
||||
@ApiPropertyOptional({ description: '注册类型:username用户名 mobile手机号 email邮箱', default: 'mobile' })
|
||||
@IsOptional()
|
||||
@IsIn(['username', 'mobile', 'email'])
|
||||
regType?: string = 'mobile';
|
||||
|
||||
@ApiPropertyOptional({ description: '短信验证码' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
smsCode?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '邮箱验证码' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
emailCode?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '图形验证码' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
captcha?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '验证码key' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
captchaKey?: string;
|
||||
}
|
||||
40
wwjcloud/src/common/auth/guards/global-auth.guard.ts
Normal file
40
wwjcloud/src/common/auth/guards/global-auth.guard.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Observable } from 'rxjs';
|
||||
import { IS_PUBLIC_KEY } from '../decorators/auth.decorator';
|
||||
|
||||
/**
|
||||
* 全局认证守卫
|
||||
* 统一处理JWT认证,支持公开路由跳过认证
|
||||
*/
|
||||
@Injectable()
|
||||
export class GlobalAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
// 检查是否为公开路由
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
handleRequest(err: any, user: any, info: any, context: ExecutionContext) {
|
||||
// 如果认证失败,抛出未授权异常
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException('认证失败,请重新登录');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
34
wwjcloud/src/common/auth/guards/jwt-auth.guard.ts
Normal file
34
wwjcloud/src/common/auth/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
// 检查是否标记为公开路由
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
handleRequest(err: any, user: any, info: any, context: ExecutionContext) {
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException('未授权访问');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
5
wwjcloud/src/common/auth/guards/local-auth.guard.ts
Normal file
5
wwjcloud/src/common/auth/guards/local-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthGuard extends AuthGuard('local') {}
|
||||
93
wwjcloud/src/common/auth/guards/roles.guard.ts
Normal file
93
wwjcloud/src/common/auth/guards/roles.guard.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AdminService } from '../../admin/admin.service';
|
||||
import { RoleService } from '../../rbac/role.service';
|
||||
import { MenuService } from '../../rbac/menu.service';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private adminService: AdminService,
|
||||
private roleService: RoleService,
|
||||
private menuService: MenuService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// 获取所需的角色或权限
|
||||
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
const requiredPermissions = this.reflector.getAllAndOverride<string[]>('permissions', [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
// 如果没有设置角色或权限要求,则允许访问
|
||||
if (!requiredRoles && !requiredPermissions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
if (!user) {
|
||||
throw new ForbiddenException('用户未登录');
|
||||
}
|
||||
|
||||
// 只对管理员进行角色权限验证
|
||||
if (user.userType !== 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取用户角色
|
||||
const userRoles = await this.adminService.getUserRoles(user.userId);
|
||||
|
||||
// 检查角色权限
|
||||
if (requiredRoles && requiredRoles.length > 0) {
|
||||
const hasRole = requiredRoles.some(role =>
|
||||
userRoles.some(userRole => userRole.roleName === role)
|
||||
);
|
||||
if (!hasRole) {
|
||||
throw new ForbiddenException('权限不足:缺少所需角色');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查菜单权限
|
||||
if (requiredPermissions && requiredPermissions.length > 0) {
|
||||
// 获取用户所有角色的权限菜单
|
||||
const allMenuIds: number[] = [];
|
||||
for (const role of userRoles) {
|
||||
const menuIds = await this.roleService.getRoleMenuIds(role.roleId);
|
||||
allMenuIds.push(...menuIds);
|
||||
}
|
||||
|
||||
// 去重
|
||||
const uniqueMenuIds = [...new Set(allMenuIds)];
|
||||
|
||||
// 获取菜单详情
|
||||
const menus = await this.menuService.findByIds(uniqueMenuIds);
|
||||
const userPermissions = menus.map(menu => menu.menuKey);
|
||||
|
||||
// 检查是否有所需权限
|
||||
const hasPermission = requiredPermissions.some(permission =>
|
||||
userPermissions.includes(permission)
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new ForbiddenException('权限不足:缺少所需权限');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof ForbiddenException) {
|
||||
throw error;
|
||||
}
|
||||
throw new ForbiddenException('权限验证失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
13
wwjcloud/src/common/auth/index.ts
Normal file
13
wwjcloud/src/common/auth/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export * from './auth.module';
|
||||
export * from './auth.controller';
|
||||
export * from './user-permission.controller';
|
||||
export * from './auth.service';
|
||||
export * from './services';
|
||||
export * from './dto';
|
||||
export * from './strategies/jwt.strategy';
|
||||
export * from './strategies/local.strategy';
|
||||
export * from './guards/jwt-auth.guard';
|
||||
export * from './guards/local-auth.guard';
|
||||
export * from './guards/roles.guard';
|
||||
export * from './guards/global-auth.guard';
|
||||
export * from './decorators/auth.decorator';
|
||||
1
wwjcloud/src/common/auth/services/index.ts
Normal file
1
wwjcloud/src/common/auth/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './permission.service';
|
||||
215
wwjcloud/src/common/auth/services/permission.service.ts
Normal file
215
wwjcloud/src/common/auth/services/permission.service.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AdminService } from '../../admin/admin.service';
|
||||
import { RoleService } from '../../rbac/role.service';
|
||||
import { MenuService } from '../../rbac/menu.service';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionService {
|
||||
constructor(
|
||||
private readonly adminService: AdminService,
|
||||
private readonly roleService: RoleService,
|
||||
private readonly menuService: MenuService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 检查管理员是否有指定权限
|
||||
* @param userId 用户ID
|
||||
* @param permission 权限标识
|
||||
* @returns 是否有权限
|
||||
*/
|
||||
async checkAdminPermission(userId: number, permission: string): Promise<boolean> {
|
||||
try {
|
||||
// 获取用户信息
|
||||
const user = await this.adminService.findById(userId);
|
||||
if (!user || user.status !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 超级管理员拥有所有权限
|
||||
if (user.isAdmin === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取用户角色
|
||||
const userRoles = await this.adminService.getUserRoles(userId);
|
||||
if (!userRoles || userRoles.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查角色权限
|
||||
for (const userRole of userRoles) {
|
||||
const role = await this.roleService.findById(userRole.roleId);
|
||||
if (role && role.status === 1) {
|
||||
// 解析角色权限规则
|
||||
const rules = this.parseRules(role.rules);
|
||||
if (rules.includes(permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('检查管理员权限失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查管理员是否有指定角色
|
||||
* @param userId 用户ID
|
||||
* @param roleNames 角色名称数组
|
||||
* @returns 是否有角色
|
||||
*/
|
||||
async checkAdminRole(userId: number, roleNames: string[]): Promise<boolean> {
|
||||
try {
|
||||
// 获取用户信息
|
||||
const user = await this.adminService.findById(userId);
|
||||
if (!user || user.status !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 超级管理员拥有所有角色
|
||||
if (user.isAdmin === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取用户角色
|
||||
const userRoles = await this.adminService.getUserRoles(userId);
|
||||
if (!userRoles || userRoles.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有指定角色
|
||||
for (const userRole of userRoles) {
|
||||
const role = await this.roleService.findById(userRole.roleId);
|
||||
if (role && role.status === 1 && roleNames.includes(role.roleName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('检查管理员角色失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户菜单权限
|
||||
* @param userId 用户ID
|
||||
* @returns 菜单ID数组
|
||||
*/
|
||||
async getUserMenuIds(userId: number): Promise<number[]> {
|
||||
try {
|
||||
// 获取用户信息
|
||||
const user = await this.adminService.findById(userId);
|
||||
if (!user || user.status !== 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 超级管理员拥有所有菜单权限
|
||||
if (user.isAdmin === 1) {
|
||||
const allMenus = await this.menuService.findAll();
|
||||
return allMenus.map(menu => menu.menuId);
|
||||
}
|
||||
|
||||
// 获取用户角色
|
||||
const userRoles = await this.adminService.getUserRoles(userId);
|
||||
if (!userRoles || userRoles.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 收集所有角色的菜单权限
|
||||
const menuIds = new Set<number>();
|
||||
for (const userRole of userRoles) {
|
||||
const role = await this.roleService.findById(userRole.roleId);
|
||||
if (role && role.status === 1) {
|
||||
const rules = this.parseRules(role.rules);
|
||||
rules.forEach(rule => {
|
||||
const menuId = parseInt(rule);
|
||||
if (!isNaN(menuId)) {
|
||||
menuIds.add(menuId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(menuIds);
|
||||
} catch (error) {
|
||||
console.error('获取用户菜单权限失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户菜单树
|
||||
* @param userId 用户ID
|
||||
* @returns 菜单树
|
||||
*/
|
||||
async getUserMenuTree(userId: number): Promise<any[]> {
|
||||
try {
|
||||
const menuIds = await this.getUserMenuIds(userId);
|
||||
if (menuIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取菜单详情
|
||||
const menus = await this.menuService.findByIds(menuIds);
|
||||
|
||||
// 构建菜单树
|
||||
return this.buildMenuTree(menus);
|
||||
} catch (error) {
|
||||
console.error('获取用户菜单树失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析权限规则
|
||||
* @param rules 权限规则字符串
|
||||
* @returns 权限数组
|
||||
*/
|
||||
private parseRules(rules: string): string[] {
|
||||
try {
|
||||
if (!rules) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 尝试解析JSON格式
|
||||
if (rules.startsWith('[') || rules.startsWith('{')) {
|
||||
const parsed = JSON.parse(rules);
|
||||
return Array.isArray(parsed) ? parsed.map(String) : [];
|
||||
}
|
||||
|
||||
// 逗号分隔格式
|
||||
return rules.split(',').map(rule => rule.trim()).filter(Boolean);
|
||||
} catch (error) {
|
||||
console.error('解析权限规则失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建菜单树
|
||||
* @param menus 菜单列表
|
||||
* @param parentId 父级ID
|
||||
* @returns 菜单树
|
||||
*/
|
||||
private buildMenuTree(menus: any[], parentId: number = 0): any[] {
|
||||
const tree = [];
|
||||
|
||||
for (const menu of menus) {
|
||||
if (menu.parentId === parentId) {
|
||||
const children = this.buildMenuTree(menus, menu.menuId);
|
||||
const menuItem = {
|
||||
...menu,
|
||||
children: children.length > 0 ? children : undefined,
|
||||
};
|
||||
tree.push(menuItem);
|
||||
}
|
||||
}
|
||||
|
||||
return tree.sort((a, b) => a.sort - b.sort);
|
||||
}
|
||||
}
|
||||
63
wwjcloud/src/common/auth/strategies/jwt.strategy.ts
Normal file
63
wwjcloud/src/common/auth/strategies/jwt.strategy.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { MemberService } from '../../member/member.service';
|
||||
import { AdminService } from '../../admin/admin.service';
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: number; // 用户ID
|
||||
username: string;
|
||||
userType: 'member' | 'admin';
|
||||
siteId?: number;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly memberService: MemberService,
|
||||
private readonly adminService: AdminService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get<string>('JWT_SECRET', 'wwjcloud-secret-key'),
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload) {
|
||||
const { sub: userId, userType, username } = payload;
|
||||
|
||||
try {
|
||||
let user;
|
||||
|
||||
if (userType === 'member') {
|
||||
user = await this.memberService.findOne(userId);
|
||||
if (!user || user.status !== 1) {
|
||||
throw new UnauthorizedException('会员账户已被禁用或不存在');
|
||||
}
|
||||
} else if (userType === 'admin') {
|
||||
user = await this.adminService.findOne(userId);
|
||||
if (!user || user.status !== 1) {
|
||||
throw new UnauthorizedException('管理员账户已被禁用或不存在');
|
||||
}
|
||||
} else {
|
||||
throw new UnauthorizedException('无效的用户类型');
|
||||
}
|
||||
|
||||
// 返回用户信息,会被注入到 request.user 中
|
||||
return {
|
||||
userId: user.memberId || user.uid,
|
||||
username: user.username,
|
||||
userType,
|
||||
siteId: user.siteId,
|
||||
user, // 完整的用户信息
|
||||
};
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Token验证失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
27
wwjcloud/src/common/auth/strategies/local.strategy.ts
Normal file
27
wwjcloud/src/common/auth/strategies/local.strategy.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-local';
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private readonly authService: AuthService) {
|
||||
super({
|
||||
usernameField: 'username',
|
||||
passwordField: 'password',
|
||||
passReqToCallback: true, // 允许传递 request 对象
|
||||
});
|
||||
}
|
||||
|
||||
async validate(request: any, username: string, password: string): Promise<any> {
|
||||
const { userType = 'member' } = request.body;
|
||||
|
||||
const user = await this.authService.validateUser(username, password, userType);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('用户名或密码错误');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
172
wwjcloud/src/common/auth/user-permission.controller.ts
Normal file
172
wwjcloud/src/common/auth/user-permission.controller.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||
import { CurrentUser, CurrentUserId } from './decorators/auth.decorator';
|
||||
import { PermissionService } from './services/permission.service';
|
||||
import { AdminService } from '../admin/admin.service';
|
||||
import { MemberService } from '../member/member.service';
|
||||
|
||||
@ApiTags('用户权限管理')
|
||||
@ApiBearerAuth()
|
||||
@Controller('user-permission')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class UserPermissionController {
|
||||
constructor(
|
||||
private readonly permissionService: PermissionService,
|
||||
private readonly adminService: AdminService,
|
||||
private readonly memberService: MemberService,
|
||||
) {}
|
||||
|
||||
@Get('profile')
|
||||
@ApiOperation({ summary: '获取当前用户信息' })
|
||||
async getCurrentUserProfile(@CurrentUser() user: any) {
|
||||
try {
|
||||
if (user.userType === 'admin') {
|
||||
const adminUser = await this.adminService.findById(user.userId);
|
||||
if (!adminUser) {
|
||||
return {
|
||||
code: 404,
|
||||
message: '用户不存在',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
// 获取用户角色
|
||||
const userRoles = await this.adminService.getUserRoles(user.userId);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
...adminUser,
|
||||
password: undefined, // 不返回密码
|
||||
userType: 'admin',
|
||||
roles: userRoles,
|
||||
},
|
||||
};
|
||||
} else if (user.userType === 'member') {
|
||||
const memberUser = await this.memberService.findById(user.userId);
|
||||
if (!memberUser) {
|
||||
return {
|
||||
code: 404,
|
||||
message: '用户不存在',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
...memberUser,
|
||||
password: undefined, // 不返回密码
|
||||
userType: 'member',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: 400,
|
||||
message: '无效的用户类型',
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
code: 500,
|
||||
message: '获取用户信息失败',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Get('menus')
|
||||
@ApiOperation({ summary: '获取当前用户菜单权限' })
|
||||
async getCurrentUserMenus(@CurrentUserId() userId: number, @CurrentUser() user: any) {
|
||||
try {
|
||||
if (user.userType !== 'admin') {
|
||||
return {
|
||||
code: 403,
|
||||
message: '只有管理员用户才能获取菜单权限',
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
const menuTree = await this.permissionService.getUserMenuTree(userId);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: menuTree,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
code: 500,
|
||||
message: '获取菜单权限失败',
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Get('permissions')
|
||||
@ApiOperation({ summary: '获取当前用户权限列表' })
|
||||
async getCurrentUserPermissions(@CurrentUserId() userId: number, @CurrentUser() user: any) {
|
||||
try {
|
||||
if (user.userType !== 'admin') {
|
||||
return {
|
||||
code: 403,
|
||||
message: '只有管理员用户才能获取权限列表',
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
const menuIds = await this.permissionService.getUserMenuIds(userId);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: menuIds,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
code: 500,
|
||||
message: '获取权限列表失败',
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Get('check-permission/:permission')
|
||||
@ApiOperation({ summary: '检查用户是否有指定权限' })
|
||||
async checkUserPermission(
|
||||
@CurrentUserId() userId: number,
|
||||
@CurrentUser() user: any,
|
||||
permission: string,
|
||||
) {
|
||||
try {
|
||||
if (user.userType !== 'admin') {
|
||||
return {
|
||||
code: 403,
|
||||
message: '只有管理员用户才能检查权限',
|
||||
data: false,
|
||||
};
|
||||
}
|
||||
|
||||
const hasPermission = await this.permissionService.checkAdminPermission(
|
||||
userId,
|
||||
permission,
|
||||
);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '检查完成',
|
||||
data: hasPermission,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
code: 500,
|
||||
message: '检查权限失败',
|
||||
data: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user