Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 新增nest模板、新增server类型的vite配置 #4743

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
15 changes: 15 additions & 0 deletions apps/backend-nest/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 适配器
VITE_ADAPTER=nest

# 应用名称
VITE_APP_NAME=Vben Server

# 应用入口
VITE_APP_PATH=src

# 应用入口的导出变量
VITE_EXPORT_NAME=default

# 立即启动
VITE_IMMEDIATE=true

1 change: 1 addition & 0 deletions apps/backend-nest/.env.analyze
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_VISUALIZER=true
8 changes: 8 additions & 0 deletions apps/backend-nest/.env.development
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# 端口号
VITE_PORT=5777

# 是否开启 SWC
VITE_SWC=true

# JWT密钥
VITE_JWT_SECRET=your_jwt_secret
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 8 additions & 0 deletions apps/backend-nest/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# 端口号
VITE_PORT=3000

# 是否开启 SWC
VITE_SWC=false

# JWT密钥
VITE_JWT_SECRET=your_jwt_secret
7 changes: 7 additions & 0 deletions apps/backend-nest/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"explorer.fileNesting.patterns": {
"*.ts": "${capture}.module.ts,${capture}.service.ts,${capture}.dto.ts,${capture}.controller.ts",
"*.module.ts": "${capture}.module.ts,${capture}.service.ts,${capture}.dto.ts",
"*.service.ts": "${capture}.dto.ts"
}
}
42 changes: 42 additions & 0 deletions apps/backend-nest/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@vben/backend-nest",
"version": "0.0.1",
"description": "",
"private": true,
"license": "MIT",
"author": "",
"scripts": {
"build": "pnpm vite build --mode production",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
"imports": {
"#/*": "./src/*"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^8.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cookie-parser": "^1.4.7",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.17",
"@types/node": "catalog:",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38"
}
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
}
14 changes: 14 additions & 0 deletions apps/backend-nest/src/app/index.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';

import { AuthModule } from '#/auth';
import filters from '#/filters';
import guards from '#/guards';
import interceptors from '#/interceptor';
import { RoutesModule } from '#/routes';

@Module({
imports: [RoutesModule, AuthModule],
providers: [...guards, ...interceptors, ...filters],
})
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class AppModule {}
35 changes: 35 additions & 0 deletions apps/backend-nest/src/app/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { INestApplication } from '@nestjs/common';

import { NestFactory } from '@nestjs/core';

import plugins from '#/plugins';

import { AppModule } from './index.module';
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved

let app: INestApplication;

async function createApp() {
const app = await NestFactory.create(AppModule);

app.enableCors();

for (const plugin of plugins) {
app.use(plugin);
}
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved

return app;
}

export async function useApp() {
if (!app) {
app = await createApp();
}

return app;
}
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved

export async function bootstrap() {
const app = await useApp();

await app.listen(import.meta.env.VITE_PORT);
}
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
22 changes: 22 additions & 0 deletions apps/backend-nest/src/auth/index.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Global, Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';

import { AuthService } from './index.service';
import { JwtStrategy } from './jwt.strategy';
import { LocalStrategy } from './local.strategy';

@Module({
imports: [
PassportModule,
JwtModule.register({
global: true,
secret: import.meta.env.VITE_JWT_SECRET,
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
@Global()
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class AuthModule {}
90 changes: 90 additions & 0 deletions apps/backend-nest/src/auth/index.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

export interface UserInfo {
id: number;
password: string;
realName: string;
roles: string[];
username: string;
}
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved

@Injectable()
export class AuthService {
// TODO: Replace with your own secret key
static ACCESS_TOKEN_SECRET = 'access_token_secret';
static MOCK_CODES = [
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
// super
{
codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'],
username: 'vben',
},
{
// admin
codes: ['AC_100010', 'AC_100020', 'AC_100030'],
username: 'admin',
},
{
// user
codes: ['AC_1000001', 'AC_1000002'],
username: 'jack',
},
];
static MOCK_USERS: UserInfo[] = [
{
id: 0,
password: '123456',
realName: 'Vben',
roles: ['super'],
username: 'vben',
},
{
id: 1,
password: '123456',
realName: 'Admin',
roles: ['admin'],
username: 'admin',
},
{
id: 2,
password: '123456',
realName: 'Jack',
roles: ['user'],
username: 'jack',
},
];
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
static REFRESH_TOKEN_SECRET = 'refresh_token_secret';

constructor(private readonly JwtService: JwtService) {}

public getAccessToken(user: UserInfo) {
return this.JwtService.sign(user, {
expiresIn: '7d',
});
}

public getRefreshToken(user: UserInfo) {
return this.JwtService.sign(user, {
expiresIn: '30d',
});
}
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved

public async validateUser(username: string, password: string) {
const findUser = AuthService.MOCK_USERS.find(
(item) => item.username === username && item.password === password,
);

if (!findUser) {
return;
}

return findUser;
}
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
}

declare global {
export namespace Express {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface User extends UserInfo {}
}
}
4 changes: 4 additions & 0 deletions apps/backend-nest/src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { AuthModule } from './index.module';
export { AuthService } from './index.service';

export type { UserInfo } from './index.service';
18 changes: 18 additions & 0 deletions apps/backend-nest/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: import.meta.env.VITE_JWT_SECRET,
});
}
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved

async validate(payload: any) {
return { ...payload };
}
}
22 changes: 22 additions & 0 deletions apps/backend-nest/src/auth/local.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';

import { AuthService } from './index.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly AuthService: AuthService) {
super();
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
}

async validate(username: string, password: string): Promise<any> {
const findUser = await this.AuthService.validateUser(username, password);

if (!findUser) {
throw new ForbiddenException('用户名或密码错误');
}

return findUser;
}
}
45 changes: 45 additions & 0 deletions apps/backend-nest/src/filters/http-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';
import type { Response } from 'express';

import { BadRequestException, Catch, HttpException } from '@nestjs/common';

import { ResponseClass } from '#/interfaces/response';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
const results = exception.getResponse() as any;
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved

// eslint-disable-next-line unicorn/throw-new-error
const result = ResponseClass.Error(results.message);

// 参数校验错误,默认都是BadRequestException
const isArrayMessage = Array.isArray(results.message);
const isValidationError =
isArrayMessage &&
typeof results.message[0] === 'string' &&
results.message[0].includes('⓿');
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
if (exception instanceof BadRequestException && isValidationError) {
const message: Array<{ field: string; message: Array<string> }> = [];
results.message.forEach((item: string) => {
const [key, val] = item.split('⓿') as [string, string];
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
const findData = message.find((item) => item.field === key);
if (findData) {
findData.message.push(val);
} else {
message.push({ field: key, message: [val] });
}
});

result.error = message;
}

return response.status(status).json(result);
}
}

// 默认导出,便于glob导入
export default HttpExceptionFilter;
13 changes: 13 additions & 0 deletions apps/backend-nest/src/filters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ClassProvider, Type } from '@nestjs/common';

import { APP_FILTER } from '@nestjs/core';

const glob_result = import.meta.glob<Type>('./*.filter.ts', {
import: 'default',
eager: true,
});

export default Object.values(glob_result).map<ClassProvider>((useClass) => ({
provide: APP_FILTER,
useClass,
}));
MZ-Dlovely marked this conversation as resolved.
Show resolved Hide resolved
28 changes: 28 additions & 0 deletions apps/backend-nest/src/guards/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { type ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';

export const Public = Reflector.createDecorator<never>();

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private readonly Reflector: Reflector) {
super();
}

override canActivate(context: ExecutionContext) {
const isPublic = this.Reflector.getAllAndOverride<boolean>(Public, [
context.getHandler(),
context.getClass(),
]);

if (isPublic) {
return true;
}

return super.canActivate(context);
}
}

// 默认导出,便于glob导入
export default JwtAuthGuard;
Loading