mirror of
https://github.com/certd/certd.git
synced 2026-04-23 19:57:27 +08:00
feat: 用户套餐,用户支付功能
This commit is contained in:
@@ -5,8 +5,8 @@ import { SmsServiceFactory } from '../sms/factory.js';
|
||||
import { ISmsService } from '../sms/api.js';
|
||||
import { CodeErrorException } from '@certd/lib-server/dist/basic/exception/code-error-exception.js';
|
||||
import { EmailService } from './email-service.js';
|
||||
import { AccessService } from '../../pipeline/service/access-service.js';
|
||||
import { AccessSysGetter } from '../../pipeline/service/access-sys-getter.js';
|
||||
import { AccessService } from '@certd/lib-server';
|
||||
import { AccessSysGetter } from '@certd/lib-server';
|
||||
import { isComm } from '@certd/plus-core';
|
||||
|
||||
// {data: '<svg.../svg>', text: 'abcd'}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createDnsProvider, IDnsProvider, parseDomain } from '@certd/plugin-cert
|
||||
import { CnameProvider, CnameRecord } from '@certd/pipeline';
|
||||
import { cache, http, logger, utils } from '@certd/basic';
|
||||
|
||||
import { AccessService } from '../../pipeline/service/access-service.js';
|
||||
import { AccessService } from '@certd/lib-server';
|
||||
import { isDev } from '@certd/basic';
|
||||
import { walkTxtRecord } from '@certd/acme-client';
|
||||
import { CnameProviderService } from './cname-provider-service.js';
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
/**
|
||||
* 授权配置
|
||||
*/
|
||||
@Entity('cd_access')
|
||||
export class AccessEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@Column({ name: 'user_id', comment: '用户id' })
|
||||
userId: number;
|
||||
@Column({ comment: '名称', length: 100 })
|
||||
name: string;
|
||||
|
||||
@Column({ comment: '类型', length: 100 })
|
||||
type: string;
|
||||
|
||||
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
|
||||
setting: string;
|
||||
|
||||
@Column({ name: 'encrypt_setting', comment: '已加密设置', length: 10240, nullable: true })
|
||||
encryptSetting: string;
|
||||
|
||||
@Column({
|
||||
name: 'create_time',
|
||||
comment: '创建时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
createTime: Date;
|
||||
@Column({
|
||||
name: 'update_time',
|
||||
comment: '修改时间',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
updateTime: Date;
|
||||
}
|
||||
@@ -29,6 +29,14 @@ export class PipelineEntity {
|
||||
@Column({ comment: '启用/禁用', nullable: true, default: false })
|
||||
disabled: boolean;
|
||||
|
||||
// cert: 证书; backup: 备份; custom:自定义;
|
||||
@Column({ comment: '类型', nullable: true, default: 'cert' })
|
||||
type: string;
|
||||
|
||||
// custom: 自定义; monitor: 监控;
|
||||
@Column({ comment: '来源', nullable: true, default: 'custom' })
|
||||
from: string;
|
||||
|
||||
@Column({
|
||||
name: 'last_history_time',
|
||||
comment: '最后一次执行时间',
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { IAccessService } from '@certd/pipeline';
|
||||
|
||||
export class AccessGetter implements IAccessService {
|
||||
userId: number;
|
||||
getter: <T>(id: any, userId?: number) => Promise<T>;
|
||||
constructor(userId: number, getter: (id: any, userId: number) => Promise<any>) {
|
||||
this.userId = userId;
|
||||
this.getter = getter;
|
||||
}
|
||||
|
||||
async getById<T = any>(id: any) {
|
||||
return await this.getter<T>(id, this.userId);
|
||||
}
|
||||
|
||||
async getCommonById<T = any>(id: any) {
|
||||
return await this.getter<T>(id, 0);
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseService, PageReq, PermissionException, ValidateException } from '@certd/lib-server';
|
||||
import { AccessEntity } from '../entity/access.js';
|
||||
import { AccessDefine, accessRegistry, newAccess } from '@certd/pipeline';
|
||||
import { EncryptService } from './encrypt-service.js';
|
||||
|
||||
/**
|
||||
* 授权
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class AccessService extends BaseService<AccessEntity> {
|
||||
@InjectEntityModel(AccessEntity)
|
||||
repository: Repository<AccessEntity>;
|
||||
|
||||
@Inject()
|
||||
encryptService: EncryptService;
|
||||
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
async page(pageReq: PageReq<AccessEntity>) {
|
||||
const res = await super.page(pageReq);
|
||||
res.records = res.records.map(item => {
|
||||
delete item.encryptSetting;
|
||||
return item;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
async add(param) {
|
||||
this.encryptSetting(param, null);
|
||||
return await super.add(param);
|
||||
}
|
||||
|
||||
encryptSetting(param: any, oldSettingEntity?: AccessEntity) {
|
||||
const accessType = param.type;
|
||||
const accessDefine: AccessDefine = accessRegistry.getDefine(accessType);
|
||||
if (!accessDefine) {
|
||||
throw new ValidateException(`授权类型${accessType}不存在`);
|
||||
}
|
||||
const setting = param.setting;
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
const json = JSON.parse(setting);
|
||||
let oldSetting = {};
|
||||
let encryptSetting = {};
|
||||
const firstEncrypt = !oldSettingEntity || !oldSettingEntity.encryptSetting || oldSettingEntity.encryptSetting === '{}';
|
||||
if (oldSettingEntity) {
|
||||
oldSetting = JSON.parse(oldSettingEntity.setting || '{}');
|
||||
encryptSetting = JSON.parse(oldSettingEntity.encryptSetting || '{}');
|
||||
}
|
||||
for (const key in json) {
|
||||
//加密
|
||||
let value = json[key];
|
||||
if (value && typeof value === 'string') {
|
||||
//去除前后空格
|
||||
value = value.trim();
|
||||
json[key] = value;
|
||||
}
|
||||
const accessInputDefine = accessDefine.input[key];
|
||||
if (!accessInputDefine) {
|
||||
continue;
|
||||
}
|
||||
if (!accessInputDefine.encrypt || !value || typeof value !== 'string') {
|
||||
//定义无需加密、value为空、不是字符串 这些不需要加密
|
||||
encryptSetting[key] = {
|
||||
value: value,
|
||||
encrypt: false,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (firstEncrypt || oldSetting[key] !== value) {
|
||||
//星号保护
|
||||
const length = value.length;
|
||||
const subIndex = Math.min(2, length);
|
||||
let starLength = length - subIndex * 2;
|
||||
starLength = Math.max(2, starLength);
|
||||
const starString = '*'.repeat(starLength);
|
||||
json[key] = value.substring(0, subIndex) + starString + value.substring(value.length - subIndex);
|
||||
encryptSetting[key] = {
|
||||
value: this.encryptService.encrypt(value),
|
||||
encrypt: true,
|
||||
};
|
||||
}
|
||||
//未改变情况下,不做修改
|
||||
}
|
||||
param.encryptSetting = JSON.stringify(encryptSetting);
|
||||
param.setting = JSON.stringify(json);
|
||||
}
|
||||
/**
|
||||
* 修改
|
||||
* @param param 数据
|
||||
*/
|
||||
async update(param) {
|
||||
const oldEntity = await this.info(param.id);
|
||||
if (oldEntity == null) {
|
||||
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
|
||||
}
|
||||
this.encryptSetting(param, oldEntity);
|
||||
return await super.update(param);
|
||||
}
|
||||
|
||||
async getSimpleInfo(id: number) {
|
||||
const entity = await this.info(id);
|
||||
if (entity == null) {
|
||||
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
|
||||
}
|
||||
return {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
userId: entity.userId,
|
||||
};
|
||||
}
|
||||
|
||||
async getAccessById(id: any, checkUserId: boolean, userId?: number): Promise<any> {
|
||||
const entity = await this.info(id);
|
||||
if (entity == null) {
|
||||
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
|
||||
}
|
||||
if (checkUserId) {
|
||||
if (userId == null) {
|
||||
throw new ValidateException('userId不能为空');
|
||||
}
|
||||
if (userId !== entity.userId) {
|
||||
throw new PermissionException('您对该Access授权无访问权限');
|
||||
}
|
||||
}
|
||||
|
||||
// const access = accessRegistry.get(entity.type);
|
||||
const setting = this.decryptAccessEntity(entity);
|
||||
const input = {
|
||||
id: entity.id,
|
||||
...setting,
|
||||
};
|
||||
return newAccess(entity.type, input);
|
||||
}
|
||||
|
||||
async getById(id: any, userId: number): Promise<any> {
|
||||
return await this.getAccessById(id, true, userId);
|
||||
}
|
||||
|
||||
decryptAccessEntity(entity: AccessEntity): any {
|
||||
let setting = {};
|
||||
if (entity.encryptSetting && entity.encryptSetting !== '{}') {
|
||||
setting = JSON.parse(entity.encryptSetting);
|
||||
for (const key in setting) {
|
||||
//解密
|
||||
const encryptValue = setting[key];
|
||||
let value = encryptValue.value;
|
||||
if (encryptValue.encrypt) {
|
||||
value = this.encryptService.decrypt(value);
|
||||
}
|
||||
setting[key] = value;
|
||||
}
|
||||
} else if (entity.setting) {
|
||||
setting = JSON.parse(entity.setting);
|
||||
}
|
||||
return setting;
|
||||
}
|
||||
|
||||
getDefineList() {
|
||||
return accessRegistry.getDefineList();
|
||||
}
|
||||
|
||||
getDefineByType(type: string) {
|
||||
return accessRegistry.getDefine(type);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { IAccessService } from '@certd/pipeline';
|
||||
import { AccessService } from './access-service.js';
|
||||
|
||||
export class AccessSysGetter implements IAccessService {
|
||||
accessService: AccessService;
|
||||
constructor(accessService: AccessService) {
|
||||
this.accessService = accessService;
|
||||
}
|
||||
|
||||
async getById<T = any>(id: any) {
|
||||
return await this.accessService.getAccessById(id, false);
|
||||
}
|
||||
|
||||
async getCommonById<T = any>(id: any) {
|
||||
return await this.accessService.getAccessById(id, false);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import crypto from 'crypto';
|
||||
import { SysSettingsService } from '@certd/lib-server';
|
||||
import { SysPrivateSettings } from '@certd/lib-server';
|
||||
|
||||
/**
|
||||
* 授权
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
export class EncryptService {
|
||||
secretKey: Buffer;
|
||||
|
||||
@Inject()
|
||||
sysSettingService: SysSettingsService;
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
const privateInfo: SysPrivateSettings = await this.sysSettingService.getSetting(SysPrivateSettings);
|
||||
this.secretKey = Buffer.from(privateInfo.encryptSecret, 'base64');
|
||||
}
|
||||
|
||||
// 加密函数
|
||||
encrypt(text: string) {
|
||||
const iv = crypto.randomBytes(16); // 初始化向量
|
||||
// const secretKey = crypto.randomBytes(32);
|
||||
// const key = Buffer.from(secretKey);
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', this.secretKey, iv);
|
||||
let encrypted = cipher.update(text);
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
return iv.toString('hex') + ':' + encrypted.toString('hex');
|
||||
}
|
||||
|
||||
// 解密函数
|
||||
decrypt(encryptedText: string) {
|
||||
const textParts = encryptedText.split(':');
|
||||
const iv = Buffer.from(textParts.shift(), 'hex');
|
||||
const encrypted = Buffer.from(textParts.join(':'), 'hex');
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.secretKey), iv);
|
||||
let decrypted = decipher.update(encrypted);
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
return decrypted.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
import { Config, Inject, Provide, Scope, ScopeEnum, sleep } from '@midwayjs/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { In, MoreThan, Repository } from 'typeorm';
|
||||
import { BaseService, NeedVIPException, PageReq, SysPublicSettings, SysSettingsService, SysSiteInfo } from '@certd/lib-server';
|
||||
import {
|
||||
AccessGetter,
|
||||
AccessService,
|
||||
BaseService,
|
||||
NeedSuiteException,
|
||||
NeedVIPException,
|
||||
PageReq,
|
||||
SysPublicSettings,
|
||||
SysSettingsService,
|
||||
SysSiteInfo,
|
||||
} from '@certd/lib-server';
|
||||
import { PipelineEntity } from '../entity/pipeline.js';
|
||||
import { PipelineDetail } from '../entity/vo/pipeline-detail.js';
|
||||
import { Executor, Pipeline, ResultType, RunHistory, SysInfo, UserInfo } from '@certd/pipeline';
|
||||
import { AccessService } from './access-service.js';
|
||||
import { Executor, Pipeline, ResultType, RunHistory, RunnableCollection, SysInfo, UserInfo } from '@certd/pipeline';
|
||||
import { DbStorage } from './db-storage.js';
|
||||
import { StorageService } from './storage-service.js';
|
||||
import { Cron } from '../../cron/cron.js';
|
||||
@@ -15,26 +24,26 @@ import { HistoryLogEntity } from '../entity/history-log.js';
|
||||
import { HistoryLogService } from './history-log-service.js';
|
||||
import { EmailService } from '../../basic/service/email-service.js';
|
||||
import { UserService } from '../../sys/authority/service/user-service.js';
|
||||
import { AccessGetter } from './access-getter.js';
|
||||
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
|
||||
import { CnameProxyService } from './cname-proxy-service.js';
|
||||
import { PluginConfigGetter } from '../../plugin/service/plugin-config-getter.js';
|
||||
import dayjs from 'dayjs';
|
||||
import { DbAdapter } from '../../db/index.js';
|
||||
import { isComm, isPlus } from '@certd/plus-core';
|
||||
import { isComm } from '@certd/plus-core';
|
||||
import { logger } from '@certd/basic';
|
||||
import { UrlService } from './url-service.js';
|
||||
import { NotificationService } from './notification-service.js';
|
||||
import { NotificationGetter } from './notification-getter.js';
|
||||
import { UserSuiteService } from '@certd/commercial-core';
|
||||
import { CertInfoService } from '../../monitor/service/cert-info-service.js';
|
||||
|
||||
const runningTasks: Map<string | number, Executor> = new Map();
|
||||
const freeCount = 10;
|
||||
|
||||
/**
|
||||
* 证书申请
|
||||
*/
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Singleton)
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class PipelineService extends BaseService<PipelineEntity> {
|
||||
@InjectEntityModel(PipelineEntity)
|
||||
repository: Repository<PipelineEntity>;
|
||||
@@ -60,6 +69,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
@Inject()
|
||||
userService: UserService;
|
||||
|
||||
@Inject()
|
||||
userSuiteService: UserSuiteService;
|
||||
|
||||
@Inject()
|
||||
cron: Cron;
|
||||
|
||||
@@ -75,6 +87,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
@Inject()
|
||||
dbAdapter: DbAdapter;
|
||||
|
||||
@Inject()
|
||||
certInfoService: CertInfoService;
|
||||
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
@@ -144,26 +159,19 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
//修改
|
||||
old = await this.info(bean.id);
|
||||
}
|
||||
const pipeline = JSON.parse(bean.content || '{}');
|
||||
const isUpdate = bean.id > 0 && old != null;
|
||||
|
||||
let domains = [];
|
||||
RunnableCollection.each(pipeline.stages, (runnable: any) => {
|
||||
if (runnable.runnableType === 'step' && runnable.type.startsWith('CertApply')) {
|
||||
domains = runnable.input.domains || [];
|
||||
}
|
||||
});
|
||||
|
||||
if (!isUpdate) {
|
||||
//如果是添加,校验数量
|
||||
if (!isPlus()) {
|
||||
const count = await this.repository.count();
|
||||
if (count >= freeCount) {
|
||||
throw new NeedVIPException(`基础版最多只能创建${freeCount}条流水线`);
|
||||
}
|
||||
}
|
||||
const userId = bean.userId;
|
||||
const userIsAdmin = await this.userService.isAdmin(userId);
|
||||
if (!userIsAdmin) {
|
||||
//非管理员用户,限制pipeline数量
|
||||
const count = await this.repository.count({ where: { userId } });
|
||||
const sysPublic = await this.sysSettingsService.getSetting<SysPublicSettings>(SysPublicSettings);
|
||||
const limitUserPipelineCount = sysPublic.limitUserPipelineCount;
|
||||
if (limitUserPipelineCount && limitUserPipelineCount > 0 && count >= limitUserPipelineCount) {
|
||||
throw new NeedVIPException(`您最多只能创建${limitUserPipelineCount}条流水线`);
|
||||
}
|
||||
}
|
||||
await this.checkMaxPipelineCount(bean, pipeline, domains);
|
||||
}
|
||||
|
||||
if (!isUpdate) {
|
||||
@@ -171,19 +179,51 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
await this.addOrUpdate(bean);
|
||||
}
|
||||
await this.clearTriggers(bean.id);
|
||||
if (bean.content) {
|
||||
const pipeline = JSON.parse(bean.content);
|
||||
if (pipeline.title) {
|
||||
bean.title = pipeline.title;
|
||||
}
|
||||
pipeline.id = bean.id;
|
||||
bean.content = JSON.stringify(pipeline);
|
||||
if (pipeline.title) {
|
||||
bean.title = pipeline.title;
|
||||
}
|
||||
pipeline.id = bean.id;
|
||||
bean.content = JSON.stringify(pipeline);
|
||||
await this.addOrUpdate(bean);
|
||||
await this.registerTriggerById(bean.id);
|
||||
|
||||
//保存域名信息到certInfo表
|
||||
await this.certInfoService.updateDomains(pipeline.id, domains);
|
||||
return bean;
|
||||
}
|
||||
|
||||
private async checkMaxPipelineCount(bean: PipelineEntity, pipeline: Pipeline, domains: string[]) {
|
||||
// if (!isPlus()) {
|
||||
// const count = await this.repository.count();
|
||||
// if (count >= freeCount) {
|
||||
// throw new NeedVIPException(`基础版最多只能创建${freeCount}条流水线`);
|
||||
// }
|
||||
// }
|
||||
if (isComm()) {
|
||||
//校验pipelineCount
|
||||
const userSuite = await this.userSuiteService.getMySuiteDetail(bean.userId);
|
||||
if (userSuite?.pipelineCount.used + 1 > userSuite?.pipelineCount.max) {
|
||||
throw new NeedSuiteException(`对不起,您最多只能创建${userSuite?.pipelineCount.max}条流水线,请购买或升级套餐`);
|
||||
}
|
||||
|
||||
if (userSuite.domainCount.used + domains.length > userSuite.domainCount.max) {
|
||||
throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`);
|
||||
}
|
||||
}
|
||||
|
||||
const userId = bean.userId;
|
||||
const userIsAdmin = await this.userService.isAdmin(userId);
|
||||
if (!userIsAdmin) {
|
||||
//非管理员用户,限制pipeline数量
|
||||
const count = await this.repository.count({ where: { userId } });
|
||||
const sysPublic = await this.sysSettingsService.getSetting<SysPublicSettings>(SysPublicSettings);
|
||||
const limitUserPipelineCount = sysPublic.limitUserPipelineCount;
|
||||
if (limitUserPipelineCount && limitUserPipelineCount > 0 && count >= limitUserPipelineCount) {
|
||||
throw new NeedVIPException(`您最多只能创建${limitUserPipelineCount}条流水线`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async foreachPipeline(callback: (pipeline: PipelineEntity) => void) {
|
||||
const idEntityList = await this.repository.find({
|
||||
select: {
|
||||
@@ -578,4 +618,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
||||
{ groupId }
|
||||
);
|
||||
}
|
||||
|
||||
async getUserPipelineCount(userId) {
|
||||
return await this.repository.count({ where: { userId } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { PipelineService } from '../../pipeline/service/pipeline-service.js';
|
||||
import { CertInfoService } from '../../monitor/service/cert-info-service.js';
|
||||
import { IUsedCountService } from '@certd/commercial-core';
|
||||
import { SiteInfoService } from '../../monitor/service/site-info-service.js';
|
||||
|
||||
@Provide('myCountService')
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class MyCountService implements IUsedCountService {
|
||||
@Inject()
|
||||
pipelineService: PipelineService;
|
||||
|
||||
@Inject()
|
||||
certInfoService: CertInfoService;
|
||||
@Inject()
|
||||
siteInfoService: SiteInfoService;
|
||||
|
||||
async getUsedCount(userId: number) {
|
||||
if (!userId) {
|
||||
throw new Error('userId is required');
|
||||
}
|
||||
const pipelineCountUsed = await this.pipelineService.getUserPipelineCount(userId);
|
||||
const domainCountUsed = await this.certInfoService.getUserDomainCount(userId);
|
||||
const monitorCountUsed = await this.siteInfoService.getUserMonitorCount(userId);
|
||||
return {
|
||||
pipelineCountUsed,
|
||||
domainCountUsed,
|
||||
monitorCountUsed,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -206,6 +206,9 @@ export class UserService extends BaseService<UserEntity> {
|
||||
});
|
||||
|
||||
delete newUser.password;
|
||||
|
||||
utils.mitter.emit('register', { userId: newUser.id });
|
||||
|
||||
return newUser;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user