feat: 添加管理员以及部署内容
This commit is contained in:
parent
bc77a579c8
commit
227a3c5b7b
|
|
@ -0,0 +1,10 @@
|
|||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
name: Backend CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
# 构建和测试
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 设置 Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: 安装 pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: 安装依赖
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 代码检查
|
||||
run: |
|
||||
echo "运行代码检查..."
|
||||
# pnpm run lint
|
||||
|
||||
- name: 运行测试
|
||||
run: |
|
||||
echo "运行单元测试..."
|
||||
# pnpm run test
|
||||
|
||||
- name: 构建项目
|
||||
run: pnpm run build
|
||||
|
||||
- name: 上传构建产物
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
retention-days: 1
|
||||
|
||||
# 部署到生产环境
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 下载构建产物
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
- name: 部署到服务器
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
port: ${{ secrets.SERVER_PORT || 22 }}
|
||||
script: |
|
||||
cd /path/to/self_proj
|
||||
|
||||
# 备份数据库
|
||||
bash deploy.sh backup
|
||||
|
||||
# 拉取最新代码
|
||||
cd oauth_nest_demo
|
||||
git pull
|
||||
cd ..
|
||||
|
||||
# 重新构建并启动后端
|
||||
docker-compose up -d --build backend
|
||||
|
||||
# 等待服务启动
|
||||
sleep 15
|
||||
|
||||
# 检查服务状态
|
||||
docker-compose ps
|
||||
|
||||
- name: 健康检查
|
||||
run: |
|
||||
echo "等待服务启动..."
|
||||
sleep 10
|
||||
curl -f http://${{ secrets.SERVER_HOST }}:3000/api || exit 1
|
||||
|
||||
- name: 通知部署结果
|
||||
if: always()
|
||||
run: |
|
||||
if [ ${{ job.status }} == 'success' ]; then
|
||||
echo "✅ 后端部署成功!"
|
||||
else
|
||||
echo "❌ 后端部署失败!"
|
||||
fi
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# 构建阶段
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装 pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# 复制依赖文件
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# 安装依赖
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# 复制源码
|
||||
COPY . .
|
||||
|
||||
# 构建
|
||||
RUN pnpm run build
|
||||
|
||||
# 生产阶段
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装 pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# 复制依赖文件
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# 只安装生产依赖
|
||||
RUN pnpm install --prod --frozen-lockfile
|
||||
|
||||
# 从构建阶段复制构建产物
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3000
|
||||
|
||||
# 启动应用
|
||||
CMD ["node", "dist/main"]
|
||||
|
|
@ -3,11 +3,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
import { User } from '../user/entities/user.entity';
|
||||
import { AdminService } from './services/admin.service';
|
||||
import { AdminGuard } from './guards/admin.guard';
|
||||
import { SuperAdminGuard } from './guards/super-admin.guard';
|
||||
import { AdminController } from './controllers/admin.controller';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
providers: [AdminService, AdminGuard],
|
||||
exports: [AdminService, AdminGuard],
|
||||
controllers: [AdminController],
|
||||
providers: [AdminService, AdminGuard, SuperAdminGuard],
|
||||
exports: [AdminService, AdminGuard, SuperAdminGuard],
|
||||
})
|
||||
export class CommonModule {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
ApiParam,
|
||||
ApiBody,
|
||||
} from '@nestjs/swagger';
|
||||
import { AdminService } from '../services/admin.service';
|
||||
import { SuperAdminGuard } from '../guards/super-admin.guard';
|
||||
import { AdminGuard } from '../guards/admin.guard';
|
||||
import { SetUserRoleDto } from '../dto/set-user-role.dto';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { UserRole } from '../enums/user-role.enum';
|
||||
import {
|
||||
UnauthorizedResponseDto,
|
||||
NotFoundResponseDto,
|
||||
} from '../dto/error-response.dto';
|
||||
|
||||
@ApiTags('管理员管理模块')
|
||||
@ApiBearerAuth()
|
||||
@Controller('admin')
|
||||
export class AdminController {
|
||||
constructor(private readonly adminService: AdminService) {}
|
||||
|
||||
@Get('users')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({
|
||||
summary: '获取所有管理员用户',
|
||||
description: '查询所有管理员和超级管理员用户列表。需要管理员权限',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '查询成功',
|
||||
type: [User],
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: '未授权,需要管理员权限',
|
||||
type: UnauthorizedResponseDto,
|
||||
})
|
||||
async getAdminUsers(): Promise<User[]> {
|
||||
return this.adminService.getAdminUsers();
|
||||
}
|
||||
|
||||
@Get('check')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({
|
||||
summary: '检查当前用户权限',
|
||||
description: '返回当前用户的角色和权限信息',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '查询成功',
|
||||
schema: {
|
||||
example: {
|
||||
userId: 1,
|
||||
role: 'admin',
|
||||
isAdmin: true,
|
||||
isSuperAdmin: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: '未授权',
|
||||
type: UnauthorizedResponseDto,
|
||||
})
|
||||
async checkPermission(@Req() req: any): Promise<{
|
||||
userId: number;
|
||||
role: UserRole | null;
|
||||
isAdmin: boolean;
|
||||
isSuperAdmin: boolean;
|
||||
}> {
|
||||
const userId = req.user?.userId || req.user?.id;
|
||||
const role = await this.adminService.getUserRole(userId);
|
||||
const isAdmin = await this.adminService.isAdmin(userId);
|
||||
const isSuperAdmin = await this.adminService.isSuperAdmin(userId);
|
||||
|
||||
return {
|
||||
userId,
|
||||
role,
|
||||
isAdmin,
|
||||
isSuperAdmin,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('users/:id/role')
|
||||
@UseGuards(SuperAdminGuard)
|
||||
@ApiOperation({
|
||||
summary: '设置用户角色(超级管理员)',
|
||||
description: '修改指定用户的角色。仅超级管理员可访问',
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: '用户ID',
|
||||
type: Number,
|
||||
})
|
||||
@ApiBody({ type: SetUserRoleDto })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '角色设置成功',
|
||||
schema: {
|
||||
example: {
|
||||
message: '用户角色已更新',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: '未授权,需要超级管理员权限',
|
||||
type: UnauthorizedResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '用户不存在',
|
||||
type: NotFoundResponseDto,
|
||||
})
|
||||
async setUserRole(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: SetUserRoleDto,
|
||||
): Promise<{ message: string; user: User }> {
|
||||
const user = await this.adminService.setUserRole(+id, dto.role);
|
||||
return {
|
||||
message: '用户角色已更新',
|
||||
user,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('users/:id/role')
|
||||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({
|
||||
summary: '查询用户角色',
|
||||
description: '获取指定用户的角色信息。需要管理员权限',
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: '用户ID',
|
||||
type: Number,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '查询成功',
|
||||
schema: {
|
||||
example: {
|
||||
userId: 1,
|
||||
role: 'admin',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 401,
|
||||
description: '未授权,需要管理员权限',
|
||||
type: UnauthorizedResponseDto,
|
||||
})
|
||||
async getUserRole(
|
||||
@Param('id') id: string,
|
||||
): Promise<{ userId: number; role: UserRole | null }> {
|
||||
const role = await this.adminService.getUserRole(+id);
|
||||
return {
|
||||
userId: +id,
|
||||
role,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||
import { UserRole } from '../enums/user-role.enum';
|
||||
|
||||
export class SetUserRoleDto {
|
||||
@ApiProperty({
|
||||
description: '用户角色',
|
||||
enum: UserRole,
|
||||
example: UserRole.ADMIN,
|
||||
})
|
||||
@IsEnum(UserRole, { message: '角色必须是有效的枚举值' })
|
||||
@IsNotEmpty({ message: '角色不能为空' })
|
||||
role: UserRole;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* 用户角色枚举
|
||||
*/
|
||||
export enum UserRole {
|
||||
/** 普通用户 */
|
||||
USER = 'user',
|
||||
/** 管理员 */
|
||||
ADMIN = 'admin',
|
||||
/** 超级管理员 */
|
||||
SUPER_ADMIN = 'super_admin',
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { AdminService } from '../services/admin.service';
|
||||
|
||||
/**
|
||||
* 超级管理员守卫
|
||||
* 仅允许超级管理员访问
|
||||
*/
|
||||
@Injectable()
|
||||
export class SuperAdminGuard implements CanActivate {
|
||||
constructor(private adminService: AdminService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
if (!user || !user.id) {
|
||||
throw new ForbiddenException('请先登录');
|
||||
}
|
||||
|
||||
const isSuperAdmin = await this.adminService.isSuperAdmin(user.id);
|
||||
|
||||
if (!isSuperAdmin) {
|
||||
throw new ForbiddenException('需要超级管理员权限');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Repository, In } from 'typeorm';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { UserRole } from '../enums/user-role.enum';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
|
|
@ -12,43 +13,77 @@ export class AdminService {
|
|||
|
||||
/**
|
||||
* 检查用户是否是管理员
|
||||
* 简单实现:检查用户名是否为 admin 或邮箱包含 admin
|
||||
* 生产环境应该使用更复杂的角色系统
|
||||
* 基于角色的权限判断:ADMIN 或 SUPER_ADMIN
|
||||
*/
|
||||
async isAdmin(userId: number): Promise<boolean> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
select: ['id', 'role', 'isActive'],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
if (!user || !user.isActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 简单的管理员判断逻辑(可根据实际需求调整)
|
||||
return (
|
||||
user.username === 'admin' ||
|
||||
user.email.includes('admin') ||
|
||||
user.username.startsWith('admin_')
|
||||
);
|
||||
return user.role === UserRole.ADMIN || user.role === UserRole.SUPER_ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否是超级管理员
|
||||
*/
|
||||
async isSuperAdmin(userId: number): Promise<boolean> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
select: ['id', 'role', 'isActive'],
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return user.role === UserRole.SUPER_ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有管理员用户
|
||||
*/
|
||||
async getAdminUsers(): Promise<User[]> {
|
||||
const users = await this.userRepository.find();
|
||||
const admins: User[] = [];
|
||||
|
||||
for (const user of users) {
|
||||
if (
|
||||
user.username === 'admin' ||
|
||||
user.email.includes('admin') ||
|
||||
user.username.startsWith('admin_')
|
||||
) {
|
||||
admins.push(user);
|
||||
}
|
||||
return this.userRepository.find({
|
||||
where: {
|
||||
role: In([UserRole.ADMIN, UserRole.SUPER_ADMIN]),
|
||||
isActive: true,
|
||||
},
|
||||
order: {
|
||||
role: 'DESC', // SUPER_ADMIN 排在前面
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return admins;
|
||||
/**
|
||||
* 设置用户角色(仅超级管理员可调用)
|
||||
*/
|
||||
async setUserRole(userId: number, role: UserRole): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
user.role = role;
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色
|
||||
*/
|
||||
async getUserRole(userId: number): Promise<UserRole | null> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: userId },
|
||||
select: ['id', 'role'],
|
||||
});
|
||||
|
||||
return user?.role || null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export class DeepseekService {
|
|||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -221,7 +221,8 @@ export class LicenseController {
|
|||
@UseGuards(AdminGuard)
|
||||
@ApiOperation({
|
||||
summary: '获取卡密统计信息(管理员)',
|
||||
description: '获取卡密的统计数据,包括总数、已激活、未使用等。仅管理员可访问',
|
||||
description:
|
||||
'获取卡密的统计数据,包括总数、已激活、未使用等。仅管理员可访问',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { UserRole } from '../../common/enums/user-role.enum';
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
|
|
@ -19,6 +20,14 @@ export class User {
|
|||
@ApiProperty({ description: '邮箱' })
|
||||
email: string;
|
||||
|
||||
@Column({ type: 'varchar', default: UserRole.USER })
|
||||
@ApiProperty({
|
||||
description: '用户角色',
|
||||
enum: UserRole,
|
||||
default: UserRole.USER,
|
||||
})
|
||||
role: UserRole;
|
||||
|
||||
@Column({ default: true })
|
||||
@ApiProperty({ description: '是否激活', default: true })
|
||||
isActive: boolean;
|
||||
|
|
|
|||
Loading…
Reference in New Issue