Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 102 additions & 92 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions template/nest-next/configs/config.example.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
{
"$schema": "./config.schema.json",
"database":{
"host": "localhost",
"port": 3306,
"user": "root",
"password": "root",
"dbName": "tinypro"
},
"redis": {
"host": "localhost",
"port": 6379,
"password": "root",
"user": "root",
"db": 0
},
"auth":{
"accessTokenTTL": 300,
"refreshTokenTTL": 86400,
"session_limit": 2,
"apiTokenTTL": 604800,
"jwt":{
"mode": "secret",
"secret": "tinypro"
}
},
"feature": {
"preview": false
}
}
108 changes: 108 additions & 0 deletions template/nest-next/configs/config.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Configure",
"type": "object",
"required": ["database", "redis", "feature", "auth"],
"properties": {
"database": { "$ref": "#/definitions/DatabaseConfig" },
"redis": { "$ref": "#/definitions/RedisConfigure" },
"feature": { "$ref": "#/definitions/FeatureConfigure" },
"swagger": { "$ref": "#/definitions/SwaggerConfigure" },
"auth": { "$ref": "#/definitions/AuthConfigure" }
},
"definitions": {
"DatabaseConfig": {
"type": "object",
"required": ["host", "port", "user", "password", "dbName"],
"properties": {
"host": { "type": "string", "description": "数据库主机地址" },
"port": { "type": "integer", "description": "数据库端口" },
"user": { "type": "string", "description": "数据库用户名" },
"password": { "type": "string", "description": "数据库密码" },
"dbName": { "type": "string", "description": "数据库名称" }
}
},
"RedisConfigure": {
"type": "object",
"required": ["host", "port", "user", "password", "db"],
"properties": {
"host": { "type": "string", "description": "Redis主机地址" },
"port": { "type": "integer", "description": "Redis端口" },
"user": { "type": "string", "description": "Redis用户名" },
"password": { "type": "string", "description": "Redis密码" },
"db": { "type": "integer", "description": "Redis数据库编号" }
}
},
"FeatureConfigure": {
"type": "object",
"required": ["preview"],
"properties": {
"preview": { "type": "boolean", "description": "预览功能开关" }
}
},
"SwaggerConfigure": {
"type": "object",
"properties": {
"title": { "type": "string", "description": "Swagger文档标题" },
"version": { "type": "string", "description": "API版本号" },
"description": { "type": "string", "description": "Swagger文档描述" }
}
},
"AuthConfigure": {
"type": "object",
"required": [
"session_limit",
"accessTokenTTL",
"refreshTokenTTL",
"apiTokenTTL",
"jwt"
],
"properties": {
"device_limit": {
"type": "integer",
"description": "设备限制数量",
"deprecated": true
},
"session_limit": { "type": "integer", "description": "会话限制数量" },
"accessTokenTTL": {
"type": "integer",
"description": "访问令牌过期时间(秒)"
},
"refreshTokenTTL": {
"type": "integer",
"description": "刷新令牌过期时间(秒)"
},
"apiTokenTTL": {
"type": "integer",
"description": "API令牌过期时间(秒)"
},
"jwt": {
"oneOf": [
{ "$ref": "#/definitions/SecretConfigure" },
{ "$ref": "#/definitions/LocalKeyConfigure" }
]
}
}
},
"SecretConfigure": {
"type": "object",
"required": ["mode", "secret"],
"properties": {
"mode": { "const": "secret", "description": "JWT模式:使用密钥" },
"secret": { "type": "string", "description": "JWT密钥" }
}
},
"LocalKeyConfigure": {
"type": "object",
"required": ["mode", "publicKeyPath", "privateKeyPath"],
"properties": {
"mode": {
"const": "local-key",
"description": "JWT模式:使用本地密钥文件"
},
"publicKeyPath": { "type": "string", "description": "公钥文件路径" },
"privateKeyPath": { "type": "string", "description": "私钥文件路径" }
}
}
}
}
5 changes: 5 additions & 0 deletions template/nest-next/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare type RequestUser = {
user?: {
email: string;
};
};
3 changes: 2 additions & 1 deletion template/nest-next/libs/configure/src/configure.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Module } from '@nestjs/common';
import { Global, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { loader } from './loader';
import { ConfigureService } from './configure.service';

@Global()
@Module({
imports: [
ConfigModule.forRoot({
Expand Down
22 changes: 22 additions & 0 deletions template/nest-next/libs/configure/src/configure/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type AuthConfigure = {
/**
* @deprecated
*/
device_limit: number;
session_limit: number;
accessTokenTTL: number;
refreshTokenTTL: number;
apiTokenTTL: number;
jwt: JwtConfigure;
};

export type JwtConfigure = SecretConfigure | LocalKeyConfigure;
export type SecretConfigure = {
mode: 'secret';
secret: string;
};
export type LocalKeyConfigure = {
mode: 'local-key';
publicKeyPath: string;
privateKeyPath: string;
};
4 changes: 4 additions & 0 deletions template/nest-next/libs/configure/src/configure/configure.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { AuthConfigure } from './auth';
import { DatabaseConfig } from './database';
import { FeatureConfigure } from './feature';
import { RedisConfigure } from './redis';
import { SwaggerConfigure } from './swagger';

export type Configure = {
database: DatabaseConfig;
redis: RedisConfigure;
feature: FeatureConfigure;
swagger?: SwaggerConfigure;
auth: AuthConfigure;
};
3 changes: 3 additions & 0 deletions template/nest-next/libs/configure/src/configure/feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type FeatureConfigure = {
preview: boolean;
};
1 change: 1 addition & 0 deletions template/nest-next/libs/configure/src/configure/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './configure';
export * from './redis';
export * from './swagger';
export * from './feature';
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ export type I18nTranslations = {
"EXISTS": string;
"PASSWORD_INCORRECT": string;
};
"auth": {
"INVALID_TOKEN": string;
"TOKEN_EXPIRED": string;
"SESSION_EXPIRED": string;
};
"preview": {
"REJECT_THIS_REQUEST": string;
};
};
"validation": {
"NOT_EMPTY": string;
Expand Down
2 changes: 2 additions & 0 deletions template/nest-next/libs/shared/src/decorator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Permission } from './permission.decorator';
export { Reject } from './reject.decorator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common';

export const PERMISSION_KEYS = 'permissions';

export const Permission = (...permissions: string[]) =>
SetMetadata(PERMISSION_KEYS, permissions);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { SetMetadata } from '@nestjs/common';

export const Reject = () => SetMetadata('reject', true);
1 change: 1 addition & 0 deletions template/nest-next/libs/shared/src/guard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './reject.guard';
41 changes: 41 additions & 0 deletions template/nest-next/libs/shared/src/guard/reject.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { I18nTranslations } from '../.generate/i18n.generated';
import { I18nContext } from 'nestjs-i18n';
import { ConfigureService } from '@app/configure';

@Injectable()
export class RejectRequestGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly cfg: ConfigureService,
) {}
canActivate(ctx: ExecutionContext): Promise<boolean> {
if (!this.cfg.get('feature.preview')) {
return Promise.resolve(true);
}
const i18n = I18nContext.current<I18nTranslations>();
const rejectRequest = this.reflector.getAllAndOverride<boolean>('reject', [
ctx.getHandler(),
ctx.getClass(),
]);
if (!rejectRequest) {
return Promise.resolve(true);
}
if (!i18n) {
return Promise.resolve(false);
}
throw new HttpException(
i18n.t('exception.preview.REJECT_THIS_REQUEST', {
lang: I18nContext?.current()?.lang || 'enUS',
}),
HttpStatus.BAD_REQUEST,
);
}
}
41 changes: 41 additions & 0 deletions template/nest-next/lua/issue-token.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
local sessionId = ARGV[1]
local sessionIssueAt = ARGV[2]
local uid = ARGV[3]
local accessTokenJti = ARGV[4]
local accessToken = ARGV[5]
local refreshToken = ARGV[6]
local refreshTokenJti = ARGV[7]
local accessTokenTTL = tonumber(ARGV[8])
local refreshTokenTTL = tonumber(ARGV[9])

local sessionLimit = tonumber(ARGV[10])

local totalSession = redis.call('ZCARD', 'user:' .. uid .. ':session')
local removeCount = totalSession - sessionLimit + 1

if removeCount > 0 then

local removed = redis.call('zpopmin', 'user:' .. uid .. ':session', removeCount)

for i = 1, #removed, 2 do
local oldSessionId = removed[i]
local accessTokenJti = redis.call('hget', 'session:' .. oldSessionId, 'accessTokenJti')
local refreshTokenJti = redis.call('hget', 'session:' .. oldSessionId, 'refreshTokenJti')
redis.call('del', 'token:' .. accessTokenJti)
redis.call('del', 'token:' .. refreshTokenJti)
redis.call('del', 'session:' .. oldSessionId)
end
end

redis.call(
'hset',
'session:' .. sessionId,
'uid', uid,
'accessTokenJti', accessTokenJti,
'refreshTokenJti', refreshTokenJti,
'issueAt', sessionIssueAt
)

redis.call('set', 'token:' .. accessTokenJti, accessToken, 'EX', accessTokenTTL)
redis.call('set', 'token:' .. refreshTokenJti, refreshToken, 'EX', refreshTokenTTL)
redis.call('zadd', 'user:' .. uid .. ':session', sessionIssueAt, sessionId)
37 changes: 37 additions & 0 deletions template/nest-next/lua/refresh-token.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
local uid = ARGV[1]
local newSessionId = ARGV[2]
local sessionIssueAt = ARGV[3]
local oldSessionId = ARGV[4]
local newAccessTokenJti = ARGV[5]
local newRefreshTokenJti = ARGV[6]
local newAccessToken = ARGV[7]
local newRefreshToken = ARGV[8]
local accessTokenTTL = ARGV[9]
local refreshTokenTTL = ARGV[10]

if redis.call('exists', 'session:' .. oldSessionId) == 0 then
return redis.error_reply('SESSION_NOT_FOUND')
end

local oldAccessTokenJti = redis.call('hget', 'session:' .. oldSessionId, 'accessTokenJti')
local oldRefreshTokenJti = redis.call('hget', 'session:' .. oldSessionId, 'refreshTokenJti')

if redis.call('exists', 'token:' .. oldRefreshTokenJti ) == 0 then
return redis.error_reply('TOKEN_EXPIRED')
end

redis.call('zrem', 'user:' .. uid .. ':session', oldSessionId)
redis.call('del', 'token:' .. oldAccessTokenJti)
redis.call('del', 'token:' .. oldRefreshTokenJti)
redis.call('del', 'session:' .. oldSessionId)

redis.call('zadd', 'user:' .. uid .. ':session', sessionIssueAt, newSessionId)
redis.call('set', 'token:' .. newAccessTokenJti, newAccessToken, 'EX', accessTokenTTL)
redis.call('set', 'token:' .. newRefreshTokenJti, newRefreshToken, 'EX', refreshTokenTTL)
redis.call(
'hset', 'session:' .. newSessionId,
'uid', uid,
'accessTokenJti', newAccessTokenJti,
'refreshTokenJti', newRefreshTokenJti,
'issueAt', sessionIssueAt
)
Loading