feat: 添加管理员以及部署内容
Backend CI/CD / build (push) Failing after 37s Details
Backend CI/CD / deploy (push) Has been skipped Details

This commit is contained in:
lichao 2026-02-24 20:17:50 +08:00
parent bc77a579c8
commit 227a3c5b7b
12 changed files with 470 additions and 25 deletions

10
.dockerignore Normal file
View File

@ -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

107
.gitea/workflows/deploy.yml Normal file
View File

@ -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

42
Dockerfile Normal file
View File

@ -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"]

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
/**
*
*/
export enum UserRole {
/** 普通用户 */
USER = 'user',
/** 管理员 */
ADMIN = 'admin',
/** 超级管理员 */
SUPER_ADMIN = 'super_admin',
}

View File

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

View File

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

View File

@ -111,7 +111,7 @@ export class DeepseekService {
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
Authorization: `Bearer ${apiKey}`,
},
},
);

View File

@ -221,7 +221,8 @@ export class LicenseController {
@UseGuards(AdminGuard)
@ApiOperation({
summary: '获取卡密统计信息(管理员)',
description: '获取卡密的统计数据,包括总数、已激活、未使用等。仅管理员可访问',
description:
'获取卡密的统计数据,包括总数、已激活、未使用等。仅管理员可访问',
})
@ApiResponse({
status: 200,

View File

@ -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;