feat: 初始化 WWJ Cloud 企业级框架项目

- 后端:基于 NestJS 的分层架构设计
- 前端:基于 VbenAdmin + Element Plus 的管理系统
- 支持 SaaS + 独立版双架构模式
- 完整的用户权限管理系统
- 系统设置、文件上传、通知等核心功能
- 多租户支持和插件化扩展架构
This commit is contained in:
万物街
2025-08-23 13:20:01 +08:00
commit f30d64e6cc
172 changed files with 10179 additions and 0 deletions

View 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,
},
};
}
}

View 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 {}

View 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: '登出成功',
};
}
}

View 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;
},
);

View 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;
}

View File

@@ -0,0 +1,3 @@
export * from './login.dto';
export * from './register.dto';
export * from './change-password.dto';

View 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;
}

View 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;
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

View 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('权限验证失败');
}
}
}

View 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';

View File

@@ -0,0 +1 @@
export * from './permission.service';

View 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);
}
}

View 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验证失败');
}
}
}

View 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;
}
}

View 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,
};
}
}
}