增加邮件注册登录相关接口
This commit is contained in:
parent
6e9fe44134
commit
aa1177b802
@ -9,7 +9,7 @@ GOOGLE_OAUTH_CLIENT2="53206975661-ih3r0ubph3rqejdq97b029difbrk2bqj.apps.googleus
|
||||
GOOGLE_OAUTH_CLIENT_IOS="53206975661-qan0rnefniegjv53ohild375pv0p7ekd.apps.googleusercontent.com"
|
||||
DB_MAIN=mongodb://localhost/wallet-development
|
||||
|
||||
EMAIL_VERIFY_URL="http://127.0.0.1:3007"
|
||||
EMAIL_VERIFY_URL="https://wallet.cebggame.com"
|
||||
|
||||
ALCHEMY_APPID="f83Is2y7L425rxl8"
|
||||
ALCHEMY_APP_SECRET="4Yn8RkxDXN71Q3p0"
|
||||
|
@ -29,11 +29,13 @@
|
||||
"mongoose": "^6.6.5",
|
||||
"mongoose-findorcreate": "^3.0.0",
|
||||
"nanoid": "^3.1.23",
|
||||
"node-schedule": "^2.1.1",
|
||||
"tracer": "^1.1.6",
|
||||
"verify-apple-id-token": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typegoose/typegoose": "^9.12.1",
|
||||
"@types/node-schedule": "^2.1.0",
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
|
@ -6,6 +6,7 @@ import { mongoose } from '@typegoose/typegoose'
|
||||
import logger from 'logger/logger'
|
||||
import config from 'config/config'
|
||||
import { ConnectOptions } from 'mongoose'
|
||||
import CodeTaskSchedule from 'schedule/codetask.schedule'
|
||||
|
||||
const zReqParserPlugin = require('plugins/zReqParser')
|
||||
|
||||
@ -30,11 +31,11 @@ export class ApiServer {
|
||||
this.server.register(zReqParserPlugin)
|
||||
this.server.register(helmet, { hidePoweredBy: false })
|
||||
this.server.register(zTokenParserPlugin)
|
||||
this.server.register(require("@fastify/view"), {
|
||||
this.server.register(require('@fastify/view'), {
|
||||
engine: {
|
||||
ejs: require("ejs"),
|
||||
ejs: require('ejs'),
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
this.server.register(apiAuthPlugin, {
|
||||
secret: config.api.token_secret,
|
||||
@ -83,6 +84,11 @@ export class ApiServer {
|
||||
return require(join(controllers, file))
|
||||
})
|
||||
}
|
||||
|
||||
initSchedules() {
|
||||
new CodeTaskSchedule().scheduleAll()
|
||||
}
|
||||
|
||||
async connectDB() {
|
||||
const options: ConnectOptions = {
|
||||
minPoolSize: 5,
|
||||
@ -152,7 +158,7 @@ export class ApiServer {
|
||||
self.registerRouter()
|
||||
self.setErrHandler()
|
||||
self.setFormatSend()
|
||||
// new Schedule().start();
|
||||
self.initSchedules()
|
||||
this.server.listen({ port: config.api.port, host: config.api.host }, (err: any, address: any) => {
|
||||
if (err) {
|
||||
logger.log(err)
|
||||
|
@ -36,8 +36,8 @@ class FacebookController extends BaseController {
|
||||
user.accessToken = code
|
||||
user.accessTokenExpire = result.data['expires_at']
|
||||
user.scope = data['scopes']
|
||||
user.nickname = infoRes['name']
|
||||
user.email = infoRes['email']
|
||||
if (infoRes['name']) user.nickname = infoRes['name']
|
||||
if (infoRes['email']) user.email = infoRes['email']
|
||||
let account = await Account.insertOrUpdate({ plat: PlatEnum.FACEBOOK, openId }, user)
|
||||
const ztoken = await res.jwtSign({ id: account.id })
|
||||
return { token: ztoken }
|
||||
|
163
src/controllers/mail.controller.ts
Normal file
163
src/controllers/mail.controller.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import BaseController, { ROLE_ANON } from 'common/base.controller'
|
||||
import { ZError } from 'common/ZError'
|
||||
import { role, router } from 'decorators/router'
|
||||
import logger from 'logger/logger'
|
||||
import { Account, PlatEnum } from 'modules/Account'
|
||||
import { CodeRecord, CodeStatus, CodeType, DEFAULT_CODE, DEFAULT_EXPIRE_TIME } from 'modules/CodeRecord'
|
||||
import {
|
||||
DEFAULT_REGIST_HTML,
|
||||
DEFAULT_REGIST_SUBJECT,
|
||||
DEFAULT_RESET_HTML,
|
||||
DEFAULT_RESET_SUBJECT,
|
||||
EmailSvr,
|
||||
} from 'service/email.svr'
|
||||
import { uuid } from 'utils/security.util'
|
||||
|
||||
const PASSWORD_SALT = '^6AssF(n,/]2>Iv<Nk|mEdFR'
|
||||
class MailController extends BaseController {
|
||||
/**
|
||||
* 通过邮件, 密码形式的登录
|
||||
*/
|
||||
@role(ROLE_ANON)
|
||||
@router('post /wallet/login/email')
|
||||
async loginWithEmail(req, res) {
|
||||
let { email, pass } = req.params
|
||||
if (!email || !pass) {
|
||||
throw new ZError(10, 'params mismatch')
|
||||
}
|
||||
// check Account exists
|
||||
let record = await Account.findByEmail(email)
|
||||
if (!record) {
|
||||
throw new ZError(11, 'account not exists')
|
||||
}
|
||||
if (record.locked) {
|
||||
throw new ZError(12, 'account locked')
|
||||
}
|
||||
if (!record.verifyPassword(pass)) {
|
||||
throw new ZError(13, 'password error')
|
||||
}
|
||||
const token = await res.jwtSign({ id: record.id })
|
||||
return { token: token }
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册email账号
|
||||
*/
|
||||
@role(ROLE_ANON)
|
||||
@router('post /email/regist')
|
||||
async registMailAccount(req, res) {
|
||||
let { email, code, password } = req.params
|
||||
if (!email || !code || !password) {
|
||||
throw new ZError(10, 'params mismatch')
|
||||
}
|
||||
let account = await Account.findByEmail(email)
|
||||
if (account) {
|
||||
throw new ZError(11, 'account exists')
|
||||
}
|
||||
let record = await CodeRecord.findByEmail(email, CodeType.REGIST)
|
||||
if (!record) {
|
||||
throw new ZError(12, 'code not exists')
|
||||
}
|
||||
if (record.status !== CodeStatus.PENDING) {
|
||||
throw new ZError(13, 'code expired')
|
||||
}
|
||||
if (record.code !== code) {
|
||||
throw new ZError(14, 'code error')
|
||||
}
|
||||
account = new Account({ plat: PlatEnum.EMAIL, email, openId: uuid() })
|
||||
account.updatePassword(password)
|
||||
account.emailReal = email
|
||||
account.emailVerified = true
|
||||
await account.save()
|
||||
record.status = CodeStatus.SUCCESS
|
||||
await record.save()
|
||||
const ztoken = await res.jwtSign({ id: account.id })
|
||||
return { token: ztoken }
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过email重置密码
|
||||
*/
|
||||
@role(ROLE_ANON)
|
||||
@router('post /email/reset_password')
|
||||
async resetMailPassword(req, res) {
|
||||
let { email, code, password } = req.params
|
||||
if (!email || !code || !password) {
|
||||
throw new ZError(10, 'params mismatch')
|
||||
}
|
||||
let account = await Account.findByEmail(email)
|
||||
if (!account) {
|
||||
throw new ZError(11, 'account not exists')
|
||||
}
|
||||
let record = await CodeRecord.findByEmail(email, CodeType.RESET)
|
||||
if (!record) {
|
||||
throw new ZError(12, 'code not exists')
|
||||
}
|
||||
if (record.status !== CodeStatus.PENDING) {
|
||||
throw new ZError(13, 'code expired')
|
||||
}
|
||||
if (record.code !== code) {
|
||||
throw new ZError(14, 'code error')
|
||||
}
|
||||
account.updatePassword(password)
|
||||
await account.save()
|
||||
record.status = CodeStatus.SUCCESS
|
||||
await record.save()
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
@role(ROLE_ANON)
|
||||
@router('post /email/send_code')
|
||||
async sendVerifyCode(req, res) {
|
||||
let { email, type } = req.params
|
||||
if (!email || !type) {
|
||||
throw new ZError(10, 'params mismatch')
|
||||
}
|
||||
type = parseInt(type)
|
||||
let record = await CodeRecord.findByEmail(email, type)
|
||||
if (!record) {
|
||||
record = new CodeRecord({ email, type, code: DEFAULT_CODE })
|
||||
await record.save()
|
||||
}
|
||||
let html = type === CodeType.REGIST ? DEFAULT_REGIST_HTML : DEFAULT_RESET_HTML
|
||||
let subject = type === CodeType.REGIST ? DEFAULT_REGIST_SUBJECT : DEFAULT_RESET_SUBJECT
|
||||
subject = record.code + ' ' + subject
|
||||
html = html.replace('{{ocde}}', record.code)
|
||||
html = html.replace('{{time}}', new Date().format('yyyy-MM-dd hh:mm:ss'))
|
||||
let msgData = {
|
||||
to: email,
|
||||
html,
|
||||
subject,
|
||||
}
|
||||
try {
|
||||
let result = await new EmailSvr().sendMail(msgData)
|
||||
record.mailSend = true
|
||||
record.emailId = result.messageId
|
||||
record.expiredAt = Date.now() + DEFAULT_EXPIRE_TIME
|
||||
await record.save()
|
||||
} catch (err) {
|
||||
logger.info(`error send mail:: emial: ${email}, type: ${type}`)
|
||||
logger.error(err)
|
||||
record.status = CodeStatus.FAIL
|
||||
await record.save()
|
||||
throw new ZError(14, 'send mail error')
|
||||
}
|
||||
return {}
|
||||
}
|
||||
/**
|
||||
* 检查email是否已经被注册
|
||||
*/
|
||||
@role(ROLE_ANON)
|
||||
@router('post /email/check')
|
||||
async checkMailExists(req, res) {
|
||||
let { email } = req.params
|
||||
if (!email) {
|
||||
throw new ZError(10, 'params mismatch')
|
||||
}
|
||||
let account = await Account.findByEmail(email)
|
||||
return { exists: !account }
|
||||
}
|
||||
}
|
@ -6,7 +6,14 @@ import { DEFAULT_VERIFY_HTML, EmailSvr } from 'service/email.svr'
|
||||
|
||||
const TOKEN_PREFIX = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'
|
||||
|
||||
/**
|
||||
* 验证已知账号的email
|
||||
* 使用发送激活链接的形式
|
||||
*/
|
||||
class VerifyController extends BaseController {
|
||||
/**
|
||||
* 发送验证email的邮件
|
||||
*/
|
||||
@router('post /email/verify')
|
||||
async sendVerifyEmail(req, res) {
|
||||
let user = req.user
|
||||
@ -29,6 +36,9 @@ class VerifyController extends BaseController {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮件认证的链接
|
||||
*/
|
||||
@role(ROLE_ANON)
|
||||
@router('get /email/verify')
|
||||
async verifyEmail(req, res) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { getModelForClass, index, modelOptions, mongoose, prop, Severity } from '@typegoose/typegoose'
|
||||
import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose'
|
||||
import { dbconn } from 'decorators/dbconn'
|
||||
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'
|
||||
import { BaseModule } from './Base'
|
||||
import { genRandomString, sha512 } from 'utils/security.util'
|
||||
|
||||
export enum PlatEnum {
|
||||
GOOGLE = 0,
|
||||
@ -9,6 +10,29 @@ export enum PlatEnum {
|
||||
TIKTOK = 2,
|
||||
FACEBOOK = 3,
|
||||
TWITTER = 4,
|
||||
EMAIL = 5,
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成密码的salt和hash
|
||||
* @param userpassword
|
||||
* @return {{salt: any, passwordHash: string}}
|
||||
*/
|
||||
export function saltHashPassword(userpassword: string) {
|
||||
let salt = genRandomString(16)
|
||||
return sha512(userpassword, salt)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
* @param userpassword
|
||||
* @param passwordDb
|
||||
* @param salt
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function verifyPass(userpassword: string, passwordDb: string, salt: string) {
|
||||
let passwordData = sha512(userpassword, salt)
|
||||
return passwordData.passwordHash === passwordDb
|
||||
}
|
||||
|
||||
interface AccountClass extends Base, TimeStamps {}
|
||||
@ -30,6 +54,12 @@ class AccountClass extends BaseModule {
|
||||
*/
|
||||
@prop()
|
||||
public email?: string
|
||||
|
||||
@prop()
|
||||
public password?: string
|
||||
|
||||
@prop()
|
||||
public salt?: string
|
||||
/**
|
||||
* 第三方登录信息中返回的email认证信息
|
||||
*/
|
||||
@ -70,6 +100,22 @@ class AccountClass extends BaseModule {
|
||||
|
||||
@prop()
|
||||
public emailVerifyTime?: number
|
||||
|
||||
public static async findByEmail(this: ReturnModelType<typeof AccountClass>, email) {
|
||||
return this.findOne({ email, plat: PlatEnum.EMAIL }).exec()
|
||||
}
|
||||
|
||||
public updatePassword(password: string) {
|
||||
if (password) {
|
||||
let passData = saltHashPassword(password)
|
||||
this.password = passData.passwordHash
|
||||
this.salt = passData.salt
|
||||
}
|
||||
}
|
||||
|
||||
public verifyPassword(password: string) {
|
||||
return verifyPass(password, this.password, this.salt)
|
||||
}
|
||||
}
|
||||
|
||||
export const Account = getModelForClass(AccountClass, { existingConnection: AccountClass.db })
|
||||
|
80
src/modules/CodeRecord.ts
Normal file
80
src/modules/CodeRecord.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { getModelForClass, index, modelOptions, pre, prop, ReturnModelType } from '@typegoose/typegoose'
|
||||
import { dbconn } from 'decorators/dbconn'
|
||||
import { BaseModule } from './Base'
|
||||
|
||||
import { customAlphabet } from 'nanoid'
|
||||
|
||||
const nanoid = customAlphabet('1234567890', 6)
|
||||
|
||||
export const DEFAULT_CODE = '000000'
|
||||
export const DEFAULT_EXPIRE_TIME = 5 * 60 * 1000
|
||||
|
||||
export enum CodeType {
|
||||
REGIST = 1, // 注册
|
||||
RESET = 2, // 重置密码
|
||||
}
|
||||
|
||||
export enum CodeStatus {
|
||||
PENDING = 1,
|
||||
SUCCESS = 2,
|
||||
FAIL = 3,
|
||||
EXPIRED = 4,
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮件验证码发送记录
|
||||
*/
|
||||
@dbconn()
|
||||
@index({ email: 1, type: 1 }, { unique: true, partialFilterExpression: { status: 1 } })
|
||||
@index({ code: 1 }, { partialFilterExpression: { status: 1 } })
|
||||
@modelOptions({
|
||||
schemaOptions: { collection: 'code_send_record', timestamps: true },
|
||||
})
|
||||
@pre<CodeRecordClass>('save', async function () {
|
||||
if (this.code === DEFAULT_CODE) {
|
||||
let exists = false
|
||||
while (!exists) {
|
||||
const code = nanoid()
|
||||
const record = await CodeRecord.findByCode(code, this.type)
|
||||
if (!record) {
|
||||
exists = true
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
class CodeRecordClass extends BaseModule {
|
||||
@prop({ required: true })
|
||||
public email!: string
|
||||
|
||||
@prop()
|
||||
public account?: string
|
||||
|
||||
@prop({ required: true })
|
||||
public code!: string
|
||||
|
||||
@prop({ default: Date.now() + DEFAULT_EXPIRE_TIME })
|
||||
public expiredAt?: number
|
||||
|
||||
@prop({ required: true, default: CodeType.REGIST })
|
||||
public type: CodeType
|
||||
|
||||
@prop({ required: true, default: CodeStatus.PENDING })
|
||||
public status: CodeStatus
|
||||
|
||||
@prop({ default: false })
|
||||
public mailSend: boolean
|
||||
|
||||
@prop()
|
||||
public emailId?: string
|
||||
|
||||
public static async findByCode(this: ReturnModelType<typeof CodeRecordClass>, code: string, type: CodeType) {
|
||||
return this.findOne({ code, type, status: CodeStatus.PENDING }).exec()
|
||||
}
|
||||
|
||||
public static async findByEmail(this: ReturnModelType<typeof CodeRecordClass>, email: string, type: CodeType) {
|
||||
return this.findOne({ email, type, status: CodeStatus.PENDING }).exec()
|
||||
}
|
||||
}
|
||||
|
||||
export const CodeRecord = getModelForClass(CodeRecordClass, { existingConnection: CodeRecordClass.db })
|
19
src/schedule/codetask.schedule.ts
Normal file
19
src/schedule/codetask.schedule.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { singleton } from 'decorators/singleton'
|
||||
import { CodeRecord, CodeStatus } from 'modules/CodeRecord'
|
||||
import * as schedule from 'node-schedule'
|
||||
|
||||
/**
|
||||
* 定时更新发送邮件验证码的过期状态
|
||||
*/
|
||||
@singleton
|
||||
export default class CodeTaskSchedule {
|
||||
async parseAllRecord() {
|
||||
let now = Date.now()
|
||||
await CodeRecord.updateMany({ expiredAt: { $lt: now }, status: CodeStatus.PENDING }, { status: CodeStatus.EXPIRED })
|
||||
}
|
||||
scheduleAll() {
|
||||
const job = schedule.scheduleJob('*/1 * * * *', async () => {
|
||||
await this.parseAllRecord()
|
||||
})
|
||||
}
|
||||
}
|
@ -1,13 +1,31 @@
|
||||
import {singleton} from "decorators/singleton";
|
||||
import { NetClient} from "net/NetClient";
|
||||
import { singleton } from 'decorators/singleton'
|
||||
import { NetClient } from 'net/NetClient'
|
||||
|
||||
export const DEFAULT_VERIFY_HTML =
|
||||
`
|
||||
export const DEFAULT_VERIFY_HTML = `
|
||||
<h1>Email Verification</h1>
|
||||
<p>CEBG needs to confirm your email address is still valid. Please click the link below to confirm you received this mail.</p>
|
||||
<p><a href="{{href}}" target="_blank">Verify Email</a></p>
|
||||
<p>If you're worried about this email being legitimate, you can visit CEBG directly to confirm your email needs verifying. After doing so, please don't forget to click the link above.</p>
|
||||
`
|
||||
|
||||
export const DEFAULT_REGIST_SUBJECT = 'CEBG regist code'
|
||||
export const DEFAULT_REGIST_HTML = `
|
||||
<h1>Your CEBG regist code is</h1>
|
||||
<h1>{{ocde}}</h1>
|
||||
<p>{{time}}</p>
|
||||
<p>This is your one time code that expires in 5 minutes.</p>
|
||||
<p>Use it to login CEBG. Never share this code with anyone.</p>
|
||||
`
|
||||
|
||||
export const DEFAULT_RESET_SUBJECT = 'CEBG reset password code'
|
||||
export const DEFAULT_RESET_HTML = `
|
||||
<h1>Your CEBG reset password code is</h1>
|
||||
<h1>{{ocde}}</h1>
|
||||
<p>{{time}}</p>
|
||||
<p>This is your one time code that expires in 5 minutes.</p>
|
||||
<p>Use it to login CEBG. Never share this code with anyone.</p>
|
||||
`
|
||||
|
||||
export interface IMailData {
|
||||
from?: string
|
||||
to: string
|
||||
@ -19,7 +37,7 @@ export interface IMailData {
|
||||
const DEFAULT_MSG_DATA: IMailData = {
|
||||
from: 'CEBG <noreply@cebg.games>',
|
||||
to: '',
|
||||
subject: 'Please verify your email address'
|
||||
subject: 'Please verify your email address',
|
||||
}
|
||||
const MAIL_SVR = 'http://127.0.0.1:3087'
|
||||
|
||||
@ -28,10 +46,9 @@ export class EmailSvr {
|
||||
public sendMail(msg: IMailData) {
|
||||
Object(DEFAULT_MSG_DATA).zssign(msg)
|
||||
let reqData = {
|
||||
url: MAIL_SVR+'/mail/send',
|
||||
data: JSON.stringify({message: msg})
|
||||
url: MAIL_SVR + '/mail/send',
|
||||
data: JSON.stringify({ message: msg }),
|
||||
}
|
||||
return new NetClient().httpPost(reqData)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import crypto from 'crypto'
|
||||
import {compressUuid} from './string.util'
|
||||
import { compressUuid } from './string.util'
|
||||
const ENCODER = 'base64'
|
||||
const REG_KEY = /^[0-9a-fA-F]{63,64}$/
|
||||
|
||||
@ -24,6 +24,23 @@ export function aesDecrypt(encryptedText: string, password: string, iv: string)
|
||||
return decrypted + decipher.final('utf8')
|
||||
}
|
||||
|
||||
export function sha512(password: string, salt: string) {
|
||||
let hash = crypto.createHmac('sha512', salt)
|
||||
hash.update(password)
|
||||
let value = hash.digest('hex')
|
||||
return {
|
||||
salt: salt,
|
||||
passwordHash: value,
|
||||
}
|
||||
}
|
||||
|
||||
export function genRandomString(length: number) {
|
||||
return crypto
|
||||
.randomBytes(Math.ceil(length / 2))
|
||||
.toString('hex')
|
||||
.slice(0, length)
|
||||
}
|
||||
|
||||
export function uuid() {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
|
38
yarn.lock
38
yarn.lock
@ -250,6 +250,13 @@
|
||||
resolved "https://registry.npmmirror.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
|
||||
integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
|
||||
|
||||
"@types/node-schedule@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-schedule/-/node-schedule-2.1.0.tgz#60375640c0509bab963573def9d1f417f438c290"
|
||||
integrity sha512-NiTwl8YN3v/1YCKrDFSmCTkVxFDylueEqsOFdgF+vPsm+AlyJKGAo5yzX1FiOxPsZiN6/r8gJitYx2EaSuBmmg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "18.11.0"
|
||||
resolved "https://registry.npmmirror.com/@types/node/-/node-18.11.0.tgz#f38c7139247a1d619f6cc6f27b072606af7c289d"
|
||||
@ -679,6 +686,13 @@ create-require@^1.1.0:
|
||||
resolved "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||
|
||||
cron-parser@^4.2.0:
|
||||
version "4.8.1"
|
||||
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.8.1.tgz#47062ea63d21d78c10ddedb08ea4c5b6fc2750fb"
|
||||
integrity sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==
|
||||
dependencies:
|
||||
luxon "^3.2.1"
|
||||
|
||||
cross-spawn@^7.0.2:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
@ -1559,6 +1573,11 @@ loglevel@^1.8.0:
|
||||
resolved "https://registry.npmmirror.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114"
|
||||
integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==
|
||||
|
||||
long-timeout@0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514"
|
||||
integrity sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
@ -1582,6 +1601,11 @@ lru-memoizer@^2.1.4:
|
||||
lodash.clonedeep "^4.5.0"
|
||||
lru-cache "~4.0.0"
|
||||
|
||||
luxon@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48"
|
||||
integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==
|
||||
|
||||
make-error@^1.1.1:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
@ -1742,6 +1766,15 @@ node-forge@^1.3.1:
|
||||
resolved "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
|
||||
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
|
||||
|
||||
node-schedule@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-2.1.1.tgz#6958b2c5af8834954f69bb0a7a97c62b97185de3"
|
||||
integrity sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==
|
||||
dependencies:
|
||||
cron-parser "^4.2.0"
|
||||
long-timeout "0.1.1"
|
||||
sorted-array-functions "^1.3.0"
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
@ -2097,6 +2130,11 @@ sonic-boom@^3.1.0:
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
|
||||
sorted-array-functions@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5"
|
||||
integrity sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==
|
||||
|
||||
source-map-support@^0.5.12:
|
||||
version "0.5.21"
|
||||
resolved "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
||||
|
Loading…
x
Reference in New Issue
Block a user