Compare commits
7
Commits
769973517b
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fac8be4dd0 | ||
|
|
de01ffd7c7 | ||
|
|
91b5cce39b | ||
|
|
ef6380395c | ||
|
|
24cb8e9843 | ||
|
|
730b77448e | ||
|
|
0d11d2bd8a |
@@ -25,6 +25,7 @@
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.7.3",
|
||||
"@fastify/static": "^8.2.0",
|
||||
"@nestjs/bullmq": "^11.0.2",
|
||||
"@nestjs/common": "^11.1.3",
|
||||
"@nestjs/core": "^11.1.3",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
@@ -33,6 +34,7 @@
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bullmq": "^5.56.0",
|
||||
"chalk": "^5.4.1",
|
||||
"chokidar": "^4.0.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
@@ -40,13 +42,16 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"deepmerge": "^4.3.1",
|
||||
"dotenv": "^16.5.0",
|
||||
"email-templates": "^12.0.3",
|
||||
"fastify": "^5.4.0",
|
||||
"find-up": "^7.0.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"ioredis": "^5.6.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"meilisearch": "^0.51.0",
|
||||
"mysql2": "^3.14.1",
|
||||
"nodemailer": "^7.0.4",
|
||||
"ora": "^8.2.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
@@ -56,6 +61,7 @@
|
||||
"rimraf": "^6.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"tencentcloud-sdk-nodejs": "^4.1.67",
|
||||
"typeorm": "^0.3.24",
|
||||
"uuid": "^11.1.0",
|
||||
"validator": "^13.15.15",
|
||||
@@ -73,12 +79,14 @@
|
||||
"@swc/cli": "^0.7.7",
|
||||
"@swc/core": "^1.12.1",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/email-templates": "^10.0.4",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"@types/node": "^24.0.1",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
|
||||
Generated
+1719
-5
File diff suppressed because it is too large
Load Diff
@@ -3,3 +3,7 @@ export * from './content.config';
|
||||
export * from './app.config';
|
||||
export * from './meili.config';
|
||||
export * from './api.config';
|
||||
export * from './sms.config';
|
||||
export * from './smtp.config';
|
||||
export * from './redis.config';
|
||||
export * from './queue.config';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { QueueOptions } from '@/modules/core/types';
|
||||
|
||||
export const queue: () => QueueOptions = () => ({
|
||||
redis: 'default',
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { RedisOptions } from '@/modules/core/types';
|
||||
|
||||
export const redis: () => RedisOptions = () => ({
|
||||
host: '192.168.50.137',
|
||||
port: 6379,
|
||||
password: '123456&Qw',
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Configure } from '@/modules/config/configure';
|
||||
import { SmsOptions } from '@/modules/core/types';
|
||||
|
||||
export const sms: (configure: Configure) => SmsOptions = (configure) => ({
|
||||
sign: configure.env.get('SMS_CLOUD_SING', 'liuyi'),
|
||||
region: configure.env.get('SMS_CLOUD_REGION', 'ap-guangzhou'),
|
||||
appid: configure.env.get('SMS_CLOUD_APPID', 'app-id'),
|
||||
secretId: configure.env.get('SMS_CLOUD_ID', 'your-secret-id'),
|
||||
secretKey: configure.env.get('SMS_CLOUD_KEY', 'your-secret-key'),
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import path from 'path';
|
||||
|
||||
import { Configure } from '@/modules/config/configure';
|
||||
import { SmtpOptions } from '@/modules/core/types';
|
||||
|
||||
export const smtp: (configure: Configure) => SmtpOptions = (configure) => ({
|
||||
host: configure.env.get('SMTP_HOST', 'localhost'),
|
||||
user: configure.env.get('SMTP_USER', 'test'),
|
||||
password: configure.env.get('SMTP_PASSWORD', ''),
|
||||
from: configure.env.get('SMTP_FROM', '平克小站<support@localhost>'),
|
||||
port: configure.env.get('SMTP_PORT', (v) => Number(v), 25),
|
||||
secure: configure.env.get('SMTP_SSL', (v) => JSON.parse(v), false),
|
||||
// Email模板路径
|
||||
resource: path.resolve(__dirname, '../../assets/emails'),
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createUserConfig } from '@/modules/user/config';
|
||||
|
||||
export const user = createUserConfig(() => ({}));
|
||||
export const user = createUserConfig(() => ({
|
||||
hash: 10,
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
import { DynamicModule, Module } from '@nestjs/common';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { DynamicModule, Module, ModuleMetadata } from '@nestjs/common';
|
||||
|
||||
import { isArray, isNil, omit } from 'lodash';
|
||||
|
||||
import { RedisService, SmsService, SmtpService } from '@/modules/core/services';
|
||||
import type {
|
||||
QueueOptions,
|
||||
RedisOption,
|
||||
RedisOptions,
|
||||
SmsOptions,
|
||||
SmtpOptions,
|
||||
} from '@/modules/core/types';
|
||||
|
||||
import { createQueueOptions, createRedisOptions } from '@/options';
|
||||
|
||||
import { Configure } from '../config/configure';
|
||||
|
||||
@@ -6,11 +20,56 @@ import { Configure } from '../config/configure';
|
||||
export class CoreModule {
|
||||
static async forRoot(configure: Configure): Promise<DynamicModule> {
|
||||
await configure.store('app.name');
|
||||
const providers: ModuleMetadata['providers'] = [];
|
||||
const exports: ModuleMetadata['exports'] = [];
|
||||
let imports: ModuleMetadata['imports'] = [];
|
||||
const redis: RedisOption[] | undefined = createRedisOptions(
|
||||
await configure.get<RedisOptions>('redis'),
|
||||
);
|
||||
if (!isNil(redis)) {
|
||||
providers.push({
|
||||
provide: RedisService,
|
||||
useFactory: () => {
|
||||
const service = new RedisService(redis);
|
||||
service.createClients();
|
||||
return service;
|
||||
},
|
||||
});
|
||||
exports.push(RedisService);
|
||||
|
||||
const queues = createQueueOptions(await configure.get<QueueOptions>('queue'), redis);
|
||||
if (!isNil(queues)) {
|
||||
if (isArray(queues)) {
|
||||
imports = queues.map((v) => BullModule.forRoot(v.name, omit(v, 'name')));
|
||||
} else {
|
||||
imports.push(BullModule.forRoot(queues));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sms = await configure.get<SmsOptions>('sms');
|
||||
if (!isNil(sms)) {
|
||||
providers.push({
|
||||
provide: SmsService,
|
||||
useFactory: () => new SmsService(sms),
|
||||
});
|
||||
exports.push(SmsService);
|
||||
}
|
||||
|
||||
const smtp = await configure.get<SmtpOptions>('smtp');
|
||||
if (!isNil(smtp)) {
|
||||
providers.push({
|
||||
provide: SmtpService,
|
||||
useFactory: () => new SmtpService(smtp),
|
||||
});
|
||||
exports.push(SmtpService);
|
||||
}
|
||||
return {
|
||||
module: CoreModule,
|
||||
global: true,
|
||||
providers: [],
|
||||
exports: [],
|
||||
providers,
|
||||
exports,
|
||||
imports,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Paramtype, SetMetadata } from '@nestjs/common';
|
||||
import { ClassTransformOptions } from 'class-transformer';
|
||||
import { ValidationOptions } from 'class-validator';
|
||||
import { ClassTransformOptions } from '@nestjs/common/interfaces/external/class-transform-options.interface';
|
||||
import { ValidatorOptions } from '@nestjs/common/interfaces/external/validator-options.interface';
|
||||
|
||||
import { DTO_VALIDATION_OPTIONS } from '../contants';
|
||||
|
||||
export const DtoValidation = (
|
||||
options?: ValidationOptions & { transformOptions?: ClassTransformOptions } & {
|
||||
options?: ValidatorOptions & { transformOptions?: ClassTransformOptions } & {
|
||||
type?: Paramtype;
|
||||
},
|
||||
) => SetMetadata(DTO_VALIDATION_OPTIONS, options ?? {});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './redis.service';
|
||||
export * from './sms.service';
|
||||
export * from './smtp.service';
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import Redis from 'ioredis';
|
||||
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
import { RedisOption } from '@/modules/core/types';
|
||||
|
||||
@Injectable()
|
||||
export class RedisService {
|
||||
protected options: RedisOption[];
|
||||
|
||||
protected clients: Record<string, Redis> = {};
|
||||
|
||||
constructor(protected _options: RedisOption[]) {
|
||||
this.options = _options;
|
||||
}
|
||||
|
||||
async createClients() {
|
||||
this.options.map(async (option) => {
|
||||
this.clients[option.name] = new Redis(option);
|
||||
});
|
||||
}
|
||||
|
||||
getClient(name?: string): Redis {
|
||||
const key = isNil(name) ? 'default' : name;
|
||||
if (this.clients[key]) {
|
||||
return this.clients[key];
|
||||
}
|
||||
throw new Error(`Unknown client ${key}`);
|
||||
}
|
||||
|
||||
getClients() {
|
||||
return this.clients;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as tencentcloud from 'tencentcloud-sdk-nodejs';
|
||||
|
||||
import { deepMerge } from '@/modules/core/helpers';
|
||||
import { SmsOptions, SmsSendParams } from '@/modules/core/types';
|
||||
|
||||
const SmsClient = tencentcloud.sms.v20210111.Client;
|
||||
|
||||
/**
|
||||
* 腾讯云短信驱动
|
||||
*/
|
||||
@Injectable()
|
||||
export class SmsService {
|
||||
/**
|
||||
* 初始化配置
|
||||
* @param options 短信发送选项
|
||||
*/
|
||||
constructor(protected options: SmsOptions) {}
|
||||
|
||||
/**
|
||||
* 创建短信发送驱动实例
|
||||
* @param options 驱动选项
|
||||
*/
|
||||
protected makeClient(options: SmsOptions) {
|
||||
const { secretId, secretKey, region, endpoint } = options;
|
||||
return new SmsClient({
|
||||
credential: { secretId, secretKey },
|
||||
region,
|
||||
profile: {
|
||||
httpProfile: { endpoint: endpoint ?? 'sms.tencentcloudapi.com' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义通用发送参数为腾讯云短信服务发送参数
|
||||
* @param params 发送参数
|
||||
* @param options 驱动选项
|
||||
*/
|
||||
protected transSendParams(params: SmsSendParams, options: SmsOptions) {
|
||||
const { numbers, template, vars, appid, sign, ...others } = params;
|
||||
let paramSet: RecordAny = {};
|
||||
if (vars) {
|
||||
paramSet = Object.fromEntries(
|
||||
Object.entries(vars).map(([key, value]) => [key, value.toString()]),
|
||||
);
|
||||
}
|
||||
return {
|
||||
PhoneNumberSet: numbers.map((n) => {
|
||||
const phone: string[] = n.split('.');
|
||||
return `${phone[0]}${phone[1]}`;
|
||||
}),
|
||||
TemplateId: template,
|
||||
SmsSdkAppId: appid ?? options.appid,
|
||||
SignName: sign ?? options.sign,
|
||||
TemplateParamSet: Object.values(paramSet),
|
||||
...(others ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并配置并发送短信
|
||||
* @param params 短信发送参数
|
||||
* @param options 自定义驱动选项(可用于临时覆盖默认选项)
|
||||
*/
|
||||
async send<T>(params: SmsSendParams & T, options?: SmsSendParams) {
|
||||
const newOptions = deepMerge(this.options, options ?? {}) as SmsOptions;
|
||||
const client = this.makeClient(newOptions);
|
||||
return client.SendSms(this.transSendParams(params, newOptions));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import path from 'path';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import Email from 'email-templates';
|
||||
import { pick } from 'lodash';
|
||||
import mailer from 'nodemailer';
|
||||
import Mail from 'nodemailer/lib/mailer';
|
||||
import SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
|
||||
import { deepMerge } from '@/modules/core/helpers';
|
||||
import { SmtpOptions, SmtpSendParams } from '@/modules/core/types';
|
||||
|
||||
/**
|
||||
* SMTP邮件发送驱动
|
||||
*/
|
||||
@Injectable()
|
||||
export class SmtpService {
|
||||
/**
|
||||
* 初始化配置
|
||||
* @param options
|
||||
*/
|
||||
constructor(protected readonly options: SmtpOptions) {}
|
||||
|
||||
/**
|
||||
* 合并配置并发送邮件
|
||||
* @param params
|
||||
* @param options
|
||||
*/
|
||||
async send<T>(params: SmtpSendParams & T, options?: SmtpOptions) {
|
||||
const newOptions = deepMerge(this.options, options ?? {}) as SmtpOptions;
|
||||
const client = this.makeClient(newOptions);
|
||||
return this.makeSend(client, params, newOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建NodeMailer客户端
|
||||
* @param options
|
||||
*/
|
||||
protected makeClient(options: SmtpOptions) {
|
||||
const { host, secure, user, password, port } = options;
|
||||
const clientOptions: SMTPConnection.Options = {
|
||||
host,
|
||||
secure: secure ?? false,
|
||||
auth: {
|
||||
user,
|
||||
pass: password,
|
||||
},
|
||||
};
|
||||
if (!clientOptions.secure) {
|
||||
clientOptions.port = port ?? 25;
|
||||
}
|
||||
return mailer.createTransport(clientOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义通用发送参数为NodeMailer发送参数
|
||||
* @param client
|
||||
* @param params
|
||||
* @param options
|
||||
*/
|
||||
protected async makeSend(client: Mail, params: SmtpSendParams, options: SmtpOptions) {
|
||||
const tplPath = path.resolve(options.resource, params.name ?? 'custom');
|
||||
const textOnly = !params.html && params.text;
|
||||
const noHtmlToText = params.html && params.text;
|
||||
const configd: Email.EmailConfig = {
|
||||
preview: params.preview ?? false,
|
||||
send: !params.preview,
|
||||
message: { from: params.from ?? options.from ?? options.user },
|
||||
transport: client,
|
||||
subjectPrefix: params.subjectPrefix,
|
||||
textOnly,
|
||||
juiceResources: {
|
||||
preserveImportant: true,
|
||||
webResources: {
|
||||
relativeTo: tplPath,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (noHtmlToText) {
|
||||
configd.htmlToText = false;
|
||||
}
|
||||
const email = new Email(configd);
|
||||
const message = {
|
||||
...pick(params, ['from', 'to', 'reply', 'attachments', 'subject']),
|
||||
locals: params.vars,
|
||||
};
|
||||
return email.send({
|
||||
template: tplPath,
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,12 @@ import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common';
|
||||
import { IAuthGuard } from '@nestjs/passport';
|
||||
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
|
||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||
import { QueueOptions as BullMQOptions } from 'bullmq';
|
||||
import dayjs from 'dayjs';
|
||||
import Email from 'email-templates';
|
||||
import { RedisOptions as IORedisOptions } from 'ioredis';
|
||||
import { Attachment } from 'nodemailer/lib/mailer';
|
||||
import { Ora } from 'ora';
|
||||
import { StartOptions } from 'pm2';
|
||||
import { ManyToMany, ManyToOne, OneToMany, OneToOne } from 'typeorm';
|
||||
@@ -142,3 +147,112 @@ export interface DynamicRelation {
|
||||
| ReturnType<typeof ManyToMany>;
|
||||
column: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 嵌套对象
|
||||
*/
|
||||
export type NestedRecord = Record<string, RecordAny>;
|
||||
|
||||
/**
|
||||
* core模块参数选项
|
||||
*/
|
||||
export interface CoreOptions {
|
||||
database?: () => TypeOrmModuleOptions;
|
||||
sms?: () => SmsOptions;
|
||||
smtp?: () => SmtpOptions;
|
||||
redis?: () => RedisOptions;
|
||||
queue?: () => QueueOptions;
|
||||
}
|
||||
/**
|
||||
* 腾讯云短信驱动配置
|
||||
*/
|
||||
export type SmsOptions<T extends NestedRecord = RecordNever> = {
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
sign: string;
|
||||
appid: string;
|
||||
region: string;
|
||||
endpoint?: string;
|
||||
} & T;
|
||||
|
||||
/**
|
||||
* 发送接口参数
|
||||
*/
|
||||
export interface SmsSendParams {
|
||||
appid?: string;
|
||||
numbers: string[];
|
||||
template: string;
|
||||
sign?: string;
|
||||
endpoint?: string;
|
||||
vars?: Record<string, any>;
|
||||
ExtendCode?: string;
|
||||
SessionContext?: string;
|
||||
SenderId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* SMTP邮件发送配置
|
||||
*/
|
||||
export type SmtpOptions<T extends NestedRecord = RecordNever> = {
|
||||
host: string;
|
||||
user: string;
|
||||
password: string;
|
||||
// Email模板总路径
|
||||
resource: string;
|
||||
from?: string;
|
||||
port?: number;
|
||||
secure?: boolean;
|
||||
} & T;
|
||||
|
||||
/**
|
||||
* 公共发送接口配置
|
||||
*/
|
||||
export interface SmtpSendParams {
|
||||
// 模板名称
|
||||
name?: string;
|
||||
// 发信地址
|
||||
from?: string;
|
||||
// 主题
|
||||
subject?: string;
|
||||
// 目标地址
|
||||
to: string | string[];
|
||||
// 回信地址
|
||||
reply?: string;
|
||||
// 是否加载html模板
|
||||
html?: boolean;
|
||||
// 是否加载text模板
|
||||
text?: boolean;
|
||||
// 模板变量
|
||||
vars?: Record<string, any>;
|
||||
// 是否预览
|
||||
preview?: boolean | Email.PreviewEmailOpts;
|
||||
// 主题前缀
|
||||
subjectPrefix?: string;
|
||||
// 附件
|
||||
attachments?: Attachment[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis连接配置
|
||||
*/
|
||||
export type RedisOption = Omit<IORedisOptions, 'name'> & { name: string };
|
||||
|
||||
/**
|
||||
* Redis配置
|
||||
*/
|
||||
export type RedisOptions = IORedisOptions | Array<RedisOption>;
|
||||
|
||||
/**
|
||||
* 队列项配置
|
||||
*/
|
||||
export type QueueOption = Omit<BullMQOptions, 'connection'> & { redis?: string };
|
||||
|
||||
/**
|
||||
* 队列配置
|
||||
*/
|
||||
export type QueueOptions = QueueOption | Array<{ name: string } & QueueOption>;
|
||||
|
||||
/**
|
||||
* BullMQ模块注册配置
|
||||
*/
|
||||
export type BullOptions = BullMQOptions | Array<{ name: string } & BullMQOptions>;
|
||||
|
||||
@@ -19,6 +19,23 @@ export function defaultUserConfig(configure: Configure): UserConfig {
|
||||
3600 * 30,
|
||||
),
|
||||
},
|
||||
captcha: {
|
||||
sms: {
|
||||
login: {
|
||||
template: configure.env.get('SMS_LOGIN_CAPTCHA_CLOUD', 'your-id'),
|
||||
},
|
||||
register: {
|
||||
template: configure.env.get('SMS_REGISTER_CAPTCHA_CLOUD', 'your-id'),
|
||||
},
|
||||
'retrieve-password': {
|
||||
template: configure.env.get('SMS_RETRIEVE_PASSWORD_CAPTCHA_CLOUD', 'your-id'),
|
||||
},
|
||||
},
|
||||
email: {
|
||||
register: {},
|
||||
'retrieve-password': {},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -40,3 +40,68 @@ export const TokenConst = {
|
||||
};
|
||||
|
||||
export const ALLOW_GUEST = 'allowGuest';
|
||||
|
||||
/**
|
||||
* 验证码发送数据DTO验证组
|
||||
*/
|
||||
export enum CaptchaDtoGroups {
|
||||
// 发送短信登录验证码
|
||||
PHONE_LOGIN = 'phone-login',
|
||||
// 发送邮件登录验证码
|
||||
EMAIL_LOGIN = 'email-login',
|
||||
// 发送短信注册验证码
|
||||
PHONE_REGISTER = 'phone-register',
|
||||
// 发送邮件注册验证码
|
||||
EMAIL_REGISTER = 'email-register',
|
||||
// 发送找回密码的短信验证码
|
||||
PHONE_RETRIEVE_PASSWORD = 'phone-retrieve-password',
|
||||
// 发送找回密码的邮件验证码
|
||||
EMAIL_RETRIEVE_PASSWORD = 'email-retrieve-password',
|
||||
// 发送登录用户密码重置的短信验证码
|
||||
PHONE_RESET_PASSWORD = 'phone-reset-password',
|
||||
// 发送登录用户密码重置的邮件验证码
|
||||
EMAIL_RESET_PASSWORD = 'email-reset-password',
|
||||
// 发送手机号绑定或更改的短信验证码
|
||||
BOUND_PHONE = 'bound-phone',
|
||||
// 发送邮箱地址绑定或更改的邮件验证码
|
||||
BOUND_EMAIL = 'bound-email',
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码操作类别
|
||||
*/
|
||||
export enum CaptchaActionType {
|
||||
// 登录操作
|
||||
LOGIN = 'login',
|
||||
// 注册操作
|
||||
REGISTER = 'register',
|
||||
// 找回密码操作
|
||||
RETRIEVE_PASSWORD = 'retrieve-password',
|
||||
// 重置密码操作
|
||||
RESET_PASSWORD = 'reset-password',
|
||||
// 手机号或邮箱地址绑定操作
|
||||
ACCOUNT_BOUND = 'account-bound',
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码类型
|
||||
*/
|
||||
export enum CaptchaType {
|
||||
SMS = 'sms',
|
||||
EMAIL = 'email',
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码异步列队名称
|
||||
*/
|
||||
export const SEND_CAPTCHA_QUEUE = 'send-captcha-queue';
|
||||
|
||||
/**
|
||||
* 发送短信验证码任务处理名称
|
||||
*/
|
||||
export const SMS_CAPTCHA_JOB = 'sms-captcha-job';
|
||||
|
||||
/**
|
||||
* 发送邮件验证码任务处理名称
|
||||
*/
|
||||
export const EMAIL_CAPTCHA_JOB = 'mail-captcha-job';
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Request,
|
||||
SerializeOptions,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@@ -14,6 +15,8 @@ import { pick } from 'lodash';
|
||||
|
||||
import { Depends } from '@/modules/restful/decorators/depend.decorator';
|
||||
|
||||
import { UserIdInterceptor } from '@/modules/user/interceptors';
|
||||
|
||||
import { Guest } from '../decorators/guest.decorator';
|
||||
import { RequestUser } from '../decorators/user.request.decorator';
|
||||
import { UpdateAccountDto, UpdatePasswordDto } from '../dtos/account.dto';
|
||||
@@ -83,8 +86,13 @@ export class AccountController {
|
||||
*/
|
||||
@Patch()
|
||||
@ApiBearerAuth()
|
||||
@UseInterceptors(UserIdInterceptor)
|
||||
@SerializeOptions({ groups: ['user-detail'] })
|
||||
async update(@RequestUser() user: ClassToPlain<UserEntity>, @Body() data: UpdateAccountDto) {
|
||||
async update(
|
||||
@RequestUser() user: ClassToPlain<UserEntity>,
|
||||
@Body()
|
||||
data: UpdateAccountDto,
|
||||
) {
|
||||
return this.userService.update({ id: user.id, ...pick(data, ['username', 'nickname']) });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +1,24 @@
|
||||
import { PickType } from '@nestjs/swagger';
|
||||
import { OmitType, PickType } from '@nestjs/swagger';
|
||||
|
||||
import { IsDefined, IsUUID, Length } from 'class-validator';
|
||||
import { Length } from 'class-validator';
|
||||
|
||||
import { IsPassword } from '@/modules/core/constraints/password.constraint';
|
||||
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
|
||||
import { UserCommonDto } from '@/modules/user/dtos/user.common.dto';
|
||||
|
||||
import { UserValidateGroup } from '../constants';
|
||||
import { CaptchaDtoGroups, UserValidateGroup } from '../constants';
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
@DtoValidation({ groups: [UserValidateGroup.ACCOUNT_UPDATE] })
|
||||
export class UpdateAccountDto extends PickType(UserCommonDto, ['username', 'nickname']) {
|
||||
/**
|
||||
* 待更新的用户ID
|
||||
*/
|
||||
@IsUUID(undefined, { message: '用户ID格式不正确', groups: [UserValidateGroup.USER_UPDATE] })
|
||||
@IsDefined({ groups: ['update'], message: '用户ID必须指定' })
|
||||
id?: string;
|
||||
}
|
||||
@DtoValidation({ groups: [UserValidateGroup.ACCOUNT_UPDATE], whitelist: false })
|
||||
export class UpdateAccountDto extends PickType(UserCommonDto, ['username', 'nickname']) {}
|
||||
|
||||
/**
|
||||
* 更改用户密码
|
||||
*/
|
||||
@DtoValidation({ groups: [UserValidateGroup.CHANGE_PASSWORD] })
|
||||
export class UpdatePasswordDto extends PickType(UserCommonDto, ['password', 'plainPassword']) {
|
||||
/**
|
||||
* 待更新的用户ID
|
||||
*/
|
||||
@IsUUID(undefined, { message: '用户ID格式不正确', groups: [UserValidateGroup.USER_UPDATE] })
|
||||
@IsDefined({ groups: ['update'], message: '用户ID必须指定' })
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* 旧密码:用户在更改密码时需要输入的原密码
|
||||
*/
|
||||
@@ -40,3 +26,20 @@ export class UpdatePasswordDto extends PickType(UserCommonDto, ['password', 'pla
|
||||
@Length(8, 50, { message: '密码长度不得少于$constraint1', always: true })
|
||||
oldPassword: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对手机/邮箱绑定验证码进行验证
|
||||
*/
|
||||
export class AccountBoundDto extends PickType(UserCommonDto, ['code', 'phone', 'email']) {}
|
||||
|
||||
/**
|
||||
* 绑定或更改手机号验证
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.BOUND_PHONE] })
|
||||
export class PhoneBoundDto extends OmitType(AccountBoundDto, ['email'] as const) {}
|
||||
|
||||
/**
|
||||
* 绑定或更改邮箱验证
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.BOUND_EMAIL] })
|
||||
export class EmailBoundDto extends OmitType(AccountBoundDto, ['phone'] as const) {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PickType } from '@nestjs/swagger';
|
||||
|
||||
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
|
||||
import { UserValidateGroup } from '@/modules/user/constants';
|
||||
import { CaptchaDtoGroups, UserValidateGroup } from '@/modules/user/constants';
|
||||
import { UserCommonDto } from '@/modules/user/dtos/user.common.dto';
|
||||
|
||||
/**
|
||||
@@ -9,6 +9,18 @@ import { UserCommonDto } from '@/modules/user/dtos/user.common.dto';
|
||||
*/
|
||||
export class CredentialDto extends PickType(UserCommonDto, ['credential', 'password']) {}
|
||||
|
||||
/**
|
||||
* 通过手机验证码登录
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.PHONE_LOGIN] })
|
||||
export class PhoneLoginDto extends PickType(UserCommonDto, ['phone', 'code'] as const) {}
|
||||
|
||||
/**
|
||||
* 通过邮箱验证码登录
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.EMAIL_LOGIN] })
|
||||
export class EmailLoginDto extends PickType(UserCommonDto, ['email', 'code'] as const) {}
|
||||
|
||||
/**
|
||||
* 普通方式注册用户
|
||||
*/
|
||||
@@ -19,3 +31,47 @@ export class RegisterDto extends PickType(UserCommonDto, [
|
||||
'password',
|
||||
'plainPassword',
|
||||
] as const) {}
|
||||
|
||||
/**
|
||||
* 通过手机验证码注册
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.PHONE_REGISTER] })
|
||||
export class PhoneRegisterDto extends PickType(UserCommonDto, ['phone', 'code'] as const) {}
|
||||
|
||||
/**
|
||||
* 通过邮件验证码注册
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.EMAIL_REGISTER] })
|
||||
export class EmailRegisterDto extends PickType(UserCommonDto, ['email', 'code'] as const) {}
|
||||
|
||||
/**
|
||||
* 通过登录凭证找回密码
|
||||
*/
|
||||
export class RetrievePasswordDto extends PickType(UserCommonDto, [
|
||||
'credential',
|
||||
'code',
|
||||
'password',
|
||||
'plainPassword',
|
||||
] as const) {}
|
||||
|
||||
/**
|
||||
* 通过手机号找回密码
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.EMAIL_RETRIEVE_PASSWORD] })
|
||||
export class PhoneRetrievePasswordDto extends PickType(UserCommonDto, [
|
||||
'phone',
|
||||
'code',
|
||||
'password',
|
||||
'plainPassword',
|
||||
] as const) {}
|
||||
|
||||
/**
|
||||
* 通过邮箱地址找回密码
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.EMAIL_RETRIEVE_PASSWORD] })
|
||||
export class EmailRetrievePasswordDto extends PickType(UserCommonDto, [
|
||||
'email',
|
||||
'code',
|
||||
'password',
|
||||
'plainPassword',
|
||||
] as const) {}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { PickType } from '@nestjs/swagger';
|
||||
|
||||
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
|
||||
|
||||
import { CaptchaDtoGroups } from '../constants';
|
||||
|
||||
import { UserCommonDto } from './user.common.dto';
|
||||
|
||||
/**
|
||||
* 发送邮件或短信验证码消息
|
||||
*/
|
||||
export class CaptchaMessage extends PickType(UserCommonDto, ['phone', 'email']) {}
|
||||
|
||||
/**
|
||||
* 发送短信验证码DTO类型
|
||||
*/
|
||||
export class PhoneCaptchaMessageDto extends PickType(CaptchaMessage, ['phone'] as const) {}
|
||||
|
||||
/**
|
||||
* 发送邮件验证码DTO类型
|
||||
*/
|
||||
export class EmailCaptchaMessageDto extends PickType(CaptchaMessage, ['email'] as const) {}
|
||||
|
||||
/**
|
||||
* 通过已登录账户发送验证码消息
|
||||
*/
|
||||
export class UserCaptchaMessageDto extends PickType(UserCommonDto, ['type']) {}
|
||||
|
||||
/**
|
||||
* 通过用户凭证发送验证码消息
|
||||
*/
|
||||
export class CredentialCaptchaMessageDto extends PickType(UserCommonDto, ['credential']) {}
|
||||
|
||||
/**
|
||||
* 发送登录验证码短信
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.PHONE_LOGIN] })
|
||||
export class LoginPhoneCaptchaDto extends PhoneCaptchaMessageDto {}
|
||||
|
||||
/**
|
||||
* 发送登录验证码邮件
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.EMAIL_LOGIN] })
|
||||
export class LoginEmailCaptchaDto extends EmailCaptchaMessageDto {}
|
||||
|
||||
/**
|
||||
* 发送注册验证码短信
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.PHONE_REGISTER] })
|
||||
export class RegisterPhoneCaptchaDto extends PhoneCaptchaMessageDto {}
|
||||
|
||||
/**
|
||||
* 发送注册验证码邮件
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.PHONE_REGISTER] })
|
||||
export class RegisterEmailCaptchaDto extends EmailCaptchaMessageDto {}
|
||||
|
||||
/**
|
||||
* 发送找回密码短信
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.EMAIL_RETRIEVE_PASSWORD] })
|
||||
export class RetrievePasswordPhoneCaptchaDto extends PhoneCaptchaMessageDto {}
|
||||
|
||||
/**
|
||||
* 发送找回密码邮件
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.EMAIL_RETRIEVE_PASSWORD] })
|
||||
export class RetrievePasswordEmailCaptchaDto extends EmailCaptchaMessageDto {}
|
||||
|
||||
/**
|
||||
* 发送手机绑定短信
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.BOUND_PHONE] })
|
||||
export class BoundPhoneCaptchaDto extends PhoneCaptchaMessageDto {}
|
||||
|
||||
/**
|
||||
* 发送邮箱绑定邮件
|
||||
*/
|
||||
@DtoValidation({ groups: [CaptchaDtoGroups.BOUND_EMAIL] })
|
||||
export class BoundEmailCaptchaDto extends EmailCaptchaMessageDto {}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { IsEmail, IsNotEmpty, IsOptional, Length } from 'class-validator';
|
||||
import { IsEmail, IsEnum, IsNotEmpty, IsNumberString, IsOptional, Length } from 'class-validator';
|
||||
|
||||
import { IsMatch } from '@/modules/core/constraints/match.constraint';
|
||||
import { IsPassword } from '@/modules/core/constraints/password.constraint';
|
||||
import { IsMatchPhone } from '@/modules/core/constraints/phone.number.constraint';
|
||||
import { IsUnique, IsUniqueExist } from '@/modules/database/constraints';
|
||||
import { UserValidateGroup } from '@/modules/user/constants';
|
||||
import { CaptchaType, UserValidateGroup } from '@/modules/user/constants';
|
||||
import { UserEntity } from '@/modules/user/entities/user.entity';
|
||||
|
||||
/**
|
||||
@@ -111,4 +111,11 @@ export class UserCommonDto {
|
||||
@IsMatch('password', false, { message: '两次输入密码不同', always: true })
|
||||
@IsNotEmpty({ message: '请再次输入密码以确认', always: true })
|
||||
plainPassword: string;
|
||||
|
||||
@IsNumberString(undefined, { message: '验证码必须为数字', always: true })
|
||||
@Length(6, 6, { message: '验证码长度错误', always: true })
|
||||
code!: string;
|
||||
|
||||
@IsEnum(CaptchaType)
|
||||
type: CaptchaType;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { OmitType, PartialType, PickType } from '@nestjs/swagger';
|
||||
|
||||
import { IsDefined, IsEnum, IsOptional, IsUUID } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsDefined, IsEnum, IsOptional, IsUUID } from 'class-validator';
|
||||
|
||||
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
|
||||
import { toBoolean } from '@/modules/core/helpers';
|
||||
import { IsDataExist } from '@/modules/database/constraints';
|
||||
import { PermissionEntity, RoleEntity } from '@/modules/rbac/entities';
|
||||
import { PaginateWithTrashedDto } from '@/modules/restful/dtos/paginate-width-trashed.dto';
|
||||
@@ -69,6 +71,7 @@ export class UpdateUserDto extends PartialType(CreateUserDto) {
|
||||
/**
|
||||
* 查询用户列表的Query数据验证
|
||||
*/
|
||||
@DtoValidation({ type: 'query', skipMissingProperties: true })
|
||||
export class QueryUserDto extends PaginateWithTrashedDto {
|
||||
/**
|
||||
* 角色ID:根据角色来过滤用户
|
||||
@@ -96,6 +99,13 @@ export class QueryUserDto extends PaginateWithTrashedDto {
|
||||
@IsEnum(UserOrderType)
|
||||
@IsOptional()
|
||||
orderBy?: UserOrderType;
|
||||
|
||||
/**
|
||||
* 过滤激活状态
|
||||
*/
|
||||
@Transform(({ value }) => toBoolean(value))
|
||||
@IsBoolean()
|
||||
actived?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { CaptchaActionType, CaptchaType } from '@/modules/user/constants';
|
||||
|
||||
@Entity('user_captcha')
|
||||
export class CaptchaEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ comment: '验证码' })
|
||||
code!: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: CaptchaActionType,
|
||||
comment: '验证操作类型',
|
||||
})
|
||||
action!: CaptchaActionType;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: CaptchaType,
|
||||
comment: '验证码类型',
|
||||
})
|
||||
type!: CaptchaType;
|
||||
|
||||
@Column({ comment: '手机号/邮箱地址' })
|
||||
value!: string;
|
||||
|
||||
@CreateDateColumn({
|
||||
comment: '创建时间',
|
||||
})
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({
|
||||
comment: '更新时间',
|
||||
})
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -63,6 +63,9 @@ export class UserEntity {
|
||||
@Column({ comment: '用户邮箱', length: 256, nullable: true, unique: true })
|
||||
email?: string;
|
||||
|
||||
@Column({ comment: '用户状态,是否激活', default: false })
|
||||
actived?: boolean;
|
||||
|
||||
/**
|
||||
* 用户创建时间
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { CaptchaActionType, CaptchaType } from '@/modules/user/constants';
|
||||
import { CaptchaEntity } from '@/modules/user/entities/captcha.entity';
|
||||
|
||||
/**
|
||||
* 用户配置类型
|
||||
* 自定义用户模块配置
|
||||
*/
|
||||
export interface UserConfig {
|
||||
/**
|
||||
@@ -10,6 +13,8 @@ export interface UserConfig {
|
||||
* jwt token的生成配置
|
||||
*/
|
||||
jwt: JwtConfig;
|
||||
|
||||
captcha?: CustomCaptchaConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,3 +44,76 @@ export interface JwtPayload {
|
||||
*/
|
||||
iat: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认用户模块配置
|
||||
*/
|
||||
export interface DefaultUserConfig {
|
||||
hash: number;
|
||||
jwt: Pick<Required<JwtConfig>, 'tokenExpired' | 'refreshTokenExpired'>;
|
||||
captcha: DefaultCaptchaConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证码配置
|
||||
*/
|
||||
export interface CustomCaptchaConfig {
|
||||
[CaptchaType.SMS]?: {
|
||||
[key in CaptchaActionType]?: Partial<SmsCaptchaOption>;
|
||||
};
|
||||
[CaptchaType.EMAIL]?: {
|
||||
[key in CaptchaActionType]?: Partial<EmailCaptchaOption>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认验证码配置
|
||||
*/
|
||||
export interface DefaultCaptchaConfig {
|
||||
[CaptchaType.SMS]: {
|
||||
[key in CaptchaActionType]: CaptchaOption;
|
||||
};
|
||||
[CaptchaType.EMAIL]: {
|
||||
[key in CaptchaActionType]: Omit<EmailCaptchaOption, 'template'>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用验证码选项
|
||||
*/
|
||||
export interface CaptchaOption {
|
||||
limit: number; // 验证码发送间隔时间
|
||||
expired: number; // 验证码有效时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机验证码选项
|
||||
*/
|
||||
export interface SmsCaptchaOption extends CaptchaOption {
|
||||
template: string; // 云厂商短信推送模板ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮件验证码选项
|
||||
*/
|
||||
export interface EmailCaptchaOption extends CaptchaOption {
|
||||
subject: string; // 邮件主题
|
||||
template?: string; // 模板路径
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务传给消费者的数据类型
|
||||
*/
|
||||
export interface SendCaptchaQueueJob {
|
||||
captcha: { [key in keyof CaptchaEntity]: CaptchaEntity[key] };
|
||||
option: SmsCaptchaOption | EmailCaptchaOption;
|
||||
otherVars?: RecordAny;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码正确性验证
|
||||
*/
|
||||
export type CaptchaValidate<T extends RecordAny = RecordNever> = T & {
|
||||
value: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
@@ -21,3 +21,10 @@ export async function encrypt(configure: Configure, password: string) {
|
||||
export function decrypt(password: string, hashed: string) {
|
||||
return bcrypt.compareSync(password, hashed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机验证码
|
||||
*/
|
||||
export function generateCaptchaCode() {
|
||||
return Math.random().toFixed(6).slice(-6);
|
||||
}
|
||||
|
||||
+60
-5
@@ -5,7 +5,7 @@ import { NestFactory } from '@nestjs/core';
|
||||
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
|
||||
import { existsSync } from 'fs-extra';
|
||||
import { isNil } from 'lodash';
|
||||
import { isArray, isNil, omit } from 'lodash';
|
||||
|
||||
import { RbacModule } from '@/modules/rbac/rbac.module';
|
||||
import { UserModule } from '@/modules/user/user.module';
|
||||
@@ -13,7 +13,13 @@ import { UserModule } from '@/modules/user/user.module';
|
||||
import * as configs from './config';
|
||||
import { ContentModule } from './modules/content/content.module';
|
||||
import { GlobalExceptionFilter } from './modules/core/filters/global-exception.filter';
|
||||
import { CreateOptions } from './modules/core/types';
|
||||
import {
|
||||
BullOptions,
|
||||
CreateOptions,
|
||||
QueueOptions,
|
||||
RedisOption,
|
||||
RedisOptions,
|
||||
} from './modules/core/types';
|
||||
import * as dbCommands from './modules/database/commands';
|
||||
import { DatabaseModule } from './modules/database/database.module';
|
||||
import { MeiliModule } from './modules/meilisearch/meili.module';
|
||||
@@ -52,12 +58,61 @@ export const createOptions: CreateOptions = {
|
||||
if (existsSync(join(__dirname, 'metadata.js'))) {
|
||||
metadata = (await import(join(__dirname, 'metadata.js'))).default;
|
||||
}
|
||||
if (existsSync(join(__dirname, 'metadata.ts'))) {
|
||||
metadata = (await import(join(__dirname, 'metadata.ts'))).default;
|
||||
}
|
||||
await restful.factoryDocs(container, metadata);
|
||||
}
|
||||
|
||||
return container;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成Redis配置
|
||||
* @param options
|
||||
*/
|
||||
export const createRedisOptions = (options: RedisOptions): RedisOption[] | undefined => {
|
||||
if (isNil(options)) {
|
||||
return undefined;
|
||||
}
|
||||
const config: Array<RedisOption> = Array.isArray(options)
|
||||
? options
|
||||
: [{ ...options, name: 'default' }];
|
||||
if (config.length < 1) {
|
||||
return undefined;
|
||||
}
|
||||
if (isNil(config.find(({ name }) => name === 'default'))) {
|
||||
config[0].name = 'default';
|
||||
}
|
||||
|
||||
return config.reduce<RedisOption[]>((o, n) => {
|
||||
const names = o.map(({ name }) => name) as string[];
|
||||
return names.includes(n.name) ? o : [...o, n];
|
||||
}, []);
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成BullMQ模块的配置
|
||||
* @param options
|
||||
* @param redis
|
||||
*/
|
||||
export const createQueueOptions = (
|
||||
options: QueueOptions,
|
||||
redis: Array<RedisOption>,
|
||||
): BullOptions | undefined => {
|
||||
if (isNil(options) || isNil(redis)) {
|
||||
return undefined;
|
||||
}
|
||||
const names = redis.map(({ name }) => name);
|
||||
if (names.length < 1 || !names.includes('default')) {
|
||||
return undefined;
|
||||
}
|
||||
if (isArray(options)) {
|
||||
return options.map((option) => ({
|
||||
...omit(option, 'redis'),
|
||||
connection: redis.find(({ name: c }) => c === (option.redis ?? 'default')),
|
||||
}));
|
||||
}
|
||||
return {
|
||||
...omit(options, 'redis'),
|
||||
connection: redis.find(({ name: c }) => c === (options.redis ?? 'default')),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -324,7 +324,6 @@ describe('AccountController (App)', () => {
|
||||
},
|
||||
});
|
||||
const { token } = loginResult.json();
|
||||
console.log(token);
|
||||
const result = await app.inject({
|
||||
method: 'POST',
|
||||
url: `${URL_PREFIX}/account/logout`,
|
||||
@@ -449,7 +448,6 @@ describe('AccountController (App)', () => {
|
||||
username: `updated-account-${randomTag}`,
|
||||
},
|
||||
});
|
||||
console.log(result.json());
|
||||
expect(result.statusCode).toBe(200);
|
||||
const updatedUser = result.json();
|
||||
expect(updatedUser.username).toBe(`updated-account-${randomTag}`);
|
||||
|
||||
Reference in New Issue
Block a user