48 changed files with 2899 additions and 185 deletions
@ -0,0 +1,2 @@ |
|||
# DTP Chat |
|||
This project is currently being used to develop an all-new harness for developing and deploying DTP web apps. Gulp is now gone, it's based on Webpack, Nodemon and BrowserSync. |
@ -0,0 +1,73 @@ |
|||
// manifest.js
|
|||
// Copyright (C) 2022,2023 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
const DTP_COMPONENT_NAME = 'manifest'; |
|||
|
|||
import express from 'express'; |
|||
|
|||
import { SiteController } from '../../lib/site-controller.js'; |
|||
|
|||
export default class ManifestController extends SiteController { |
|||
|
|||
static get name ( ) { return 'ManifestController'; } |
|||
static get slug ( ) { return 'manifest'; } |
|||
|
|||
static create (dtp) { |
|||
const instance = new ManifestController(dtp); |
|||
return instance; |
|||
} |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, DTP_COMPONENT_NAME); |
|||
} |
|||
|
|||
async start ( ) { |
|||
const { dtp } = this; |
|||
const { limiter: limiterService } = dtp.services; |
|||
|
|||
const router = express.Router(); |
|||
dtp.app.use('/manifest.json', router); |
|||
|
|||
router.use(async (req, res, next) => { |
|||
res.locals.currentView = DTP_COMPONENT_NAME; |
|||
return next(); |
|||
}); |
|||
|
|||
router.get('/', |
|||
limiterService.create(limiterService.config.manifest.getManifest), |
|||
this.getManifest.bind(this), |
|||
); |
|||
} |
|||
|
|||
async getManifest (req, res, next) { |
|||
try { |
|||
const manifest = { |
|||
id: '/', |
|||
scope: '/', |
|||
start_url: '/', |
|||
name: this.dtp.config.site.name, |
|||
short_name: this.dtp.config.site.name, |
|||
description: this.dtp.config.site.description, |
|||
display: 'fullscreen', |
|||
theme_color: '#e8e8e8', |
|||
background_color: '#c32b2b', |
|||
icons: [ ], |
|||
}; |
|||
|
|||
[512, 384, 256, 192, 144, 96, 72, 48, 32, 16].forEach((size) => { |
|||
manifest.icons.push({ |
|||
src: `/img/icon/${this.dtp.config.site.domainKey}/icon-${size}x${size}.png`, |
|||
sizes: `${size}x${size}`, |
|||
type: 'image/png' |
|||
}); |
|||
}); |
|||
|
|||
res.status(200).json(manifest); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,71 @@ |
|||
// welcome.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
const DTP_COMPONENT_NAME = 'welcome'; |
|||
|
|||
import express from 'express'; |
|||
|
|||
import { SiteController } from '../../lib/site-controller.js'; |
|||
|
|||
export default class WelcomeController extends SiteController { |
|||
|
|||
static get name ( ) { return 'WelcomeController'; } |
|||
static get slug ( ) { return 'welcome'; } |
|||
|
|||
static create (dtp) { |
|||
const instance = new WelcomeController(dtp); |
|||
return instance; |
|||
} |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, DTP_COMPONENT_NAME); |
|||
} |
|||
|
|||
async start ( ) { |
|||
const { dtp } = this; |
|||
// const { limiter: limiterService } = dtp.services;
|
|||
|
|||
this.log.info('WelcomeController starting'); |
|||
|
|||
const router = express.Router(); |
|||
dtp.app.use('/welcome', router); |
|||
|
|||
router.use(async (req, res, next) => { |
|||
res.locals.currentView = DTP_COMPONENT_NAME; |
|||
return next(); |
|||
}); |
|||
|
|||
router.post('/signup', this.postSignup.bind(this)); |
|||
router.get('/signup', this.getSignup.bind(this)); |
|||
|
|||
router.get('/login', this.getLogin.bind(this)); |
|||
|
|||
router.get('/', this.getHome.bind(this)); |
|||
} |
|||
|
|||
async postSignup (req, res, next) { |
|||
const { user: userService } = this.dtp.services; |
|||
try { |
|||
res.locals.user = await userService.create(req.body); |
|||
res.render('welcome/signup-complete'); |
|||
} catch (error) { |
|||
this.log.error('failed to create new User account', { error }); |
|||
return next(error); |
|||
} |
|||
} |
|||
|
|||
async getSignup (req, res) { |
|||
res.render('welcome/signup'); |
|||
} |
|||
|
|||
async getLogin (req, res) { |
|||
res.render('welcome/login'); |
|||
} |
|||
|
|||
async getHome (req, res) { |
|||
res.render('welcome/home'); |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
// auth-token.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import mongoose from 'mongoose'; |
|||
const Schema = mongoose.Schema; |
|||
|
|||
const AuthTokenSchema = new Schema({ |
|||
created: { type: Date, default: Date.now, required: true, index: 1, expires: '7d' }, |
|||
claimed: { type: Date }, |
|||
purpose: { type: String, required: true }, |
|||
token: { type: String, required: true, index: 1 }, |
|||
data: { type: Schema.Types.Mixed }, |
|||
}); |
|||
|
|||
export default mongoose.model('AuthToken', AuthTokenSchema); |
@ -0,0 +1,33 @@ |
|||
// email-blacklist.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import mongoose from 'mongoose'; |
|||
const Schema = mongoose.Schema; |
|||
|
|||
const EmailBlacklistSchema = new Schema({ |
|||
created: { type: Date, required: true, default: Date.now, index: -1, expires: '30d' }, |
|||
email: { |
|||
type: String, |
|||
required: true, |
|||
lowercase: true, |
|||
maxLength: 255, |
|||
unique: true, |
|||
}, |
|||
flags: { |
|||
isVerified: { type: Boolean, default: false, required: true }, |
|||
}, |
|||
}); |
|||
|
|||
EmailBlacklistSchema.index({ |
|||
email: 1, |
|||
'flags.isVerified': 1, |
|||
}, { |
|||
partialFilterExpression: { |
|||
'flags.isVerified': true, |
|||
}, |
|||
}); |
|||
|
|||
export default mongoose.model('EmailBlacklist', EmailBlacklistSchema); |
@ -0,0 +1,19 @@ |
|||
// email-log.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import mongoose from "mongoose"; |
|||
const Schema = mongoose.Schema; |
|||
|
|||
const EmailLogSchema = new Schema({ |
|||
created: { type: Date, default: Date.now, required: true, index: -1 }, |
|||
from: { type: String, required: true, }, |
|||
to: { type: String, required: true }, |
|||
to_lc: { type: String, required: true, lowercase: true, index: 1 }, |
|||
subject: { type: String }, |
|||
messageId: { type: String }, |
|||
}); |
|||
|
|||
export default mongoose.model('EmailLog', EmailLogSchema); |
@ -0,0 +1,17 @@ |
|||
// email-verify.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import mongoose from "mongoose"; |
|||
const Schema = mongoose.Schema; |
|||
|
|||
const EmailVerifySchema = new Schema({ |
|||
created: { type: Date, default: Date.now, required: true, index: -1, expires: '30d' }, |
|||
verified: { type: Date }, |
|||
user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, |
|||
token: { type: String, required: true }, |
|||
}); |
|||
|
|||
export default mongoose.model('EmailVerify', EmailVerifySchema); |
@ -0,0 +1,20 @@ |
|||
// email.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import mongoose from "mongoose"; |
|||
const Schema = mongoose.Schema; |
|||
|
|||
const EmailSchema = new Schema({ |
|||
created: { type: Date, default: Date.now, required: true, index: -1 }, |
|||
token: { type: String, required: true, index: 1 }, |
|||
from: { type: String, required: true, }, |
|||
to: { type: String, required: true }, |
|||
to_lc: { type: String, required: true, lowercase: true, index: 1 }, |
|||
contentType: { type: String, enum: ['Newsletter'], required: true }, |
|||
content: { type: Schema.ObjectId, required: true, index: true, refPath: 'contentType' }, |
|||
}); |
|||
|
|||
export default mongoose.model('Email', EmailSchema); |
@ -0,0 +1,84 @@ |
|||
// auth-token.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import mongoose from 'mongoose'; |
|||
const AuthToken = mongoose.model('AuthToken'); |
|||
|
|||
import { v4 as uuidv4 } from 'uuid'; |
|||
|
|||
import { SiteService, SiteError } from '../../lib/site-lib.js'; |
|||
|
|||
export default class AuthTokenService extends SiteService { |
|||
|
|||
static get slug () { return 'authToken'; } |
|||
static get name ( ) { return 'AuthTokenService'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, AuthTokenService); |
|||
} |
|||
|
|||
async create (purpose, data) { |
|||
const NOW = new Date(); |
|||
const passwordResetToken = new AuthToken(); |
|||
passwordResetToken.created = NOW; |
|||
passwordResetToken.purpose = purpose; |
|||
passwordResetToken.token = uuidv4(); |
|||
if (data) { |
|||
passwordResetToken.data = data; |
|||
} |
|||
await passwordResetToken.save(); |
|||
return passwordResetToken.toObject(); |
|||
} |
|||
|
|||
async getByValue (tokenValue) { |
|||
if (!tokenValue) { |
|||
throw new SiteError(400, 'Must include an authentication token'); |
|||
} |
|||
if ((typeof tokenValue !== 'string') || (tokenValue.length !== 36)) { |
|||
throw new SiteError(400, 'The authentication token is invalid'); |
|||
} |
|||
|
|||
const token = await AuthToken |
|||
.findOne({ token: tokenValue }) |
|||
.lean(); |
|||
|
|||
if (!token) { |
|||
throw new SiteError(400, 'Auth token not found'); |
|||
} |
|||
if (token.claimed) { |
|||
throw new SiteError(403, 'Auth token already used'); |
|||
} |
|||
|
|||
return token; |
|||
} |
|||
|
|||
async claim (tokenValue) { |
|||
const token = await this.getByValue(tokenValue); |
|||
if (!token) { |
|||
throw new SiteError(403, 'The authentication token has expired'); |
|||
} |
|||
|
|||
if (token.claimed) { |
|||
throw new SiteError(403, 'This authentication token has already been claimed'); |
|||
} |
|||
|
|||
token.claimed = new Date(); |
|||
await AuthToken.updateOne({ _id: token._id }, { $set: { claimed: token.claimed } }); |
|||
|
|||
return token; // with claimed date
|
|||
} |
|||
|
|||
async removeForUser (user) { |
|||
await AuthToken.deleteMany({ user: user._id }); |
|||
} |
|||
|
|||
async remove (token) { |
|||
if (!token || !token._id) { |
|||
throw new SiteError(400, 'Must include auth token'); |
|||
} |
|||
await AuthToken.deleteOne({ _id: token._id }); |
|||
} |
|||
} |
@ -0,0 +1,84 @@ |
|||
// cache.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import { SiteService } from '../../lib/site-lib.js'; |
|||
|
|||
export default class CacheService extends SiteService { |
|||
|
|||
static get slug () { return 'cache'; } |
|||
static get name ( ) { return 'CacheService'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, CacheService); |
|||
} |
|||
|
|||
async set (name, value) { |
|||
return this.dtp.redis.set(name, value); |
|||
} |
|||
|
|||
async setEx (name, seconds, value) { |
|||
return this.dtp.redis.setex(name, seconds, value); |
|||
} |
|||
|
|||
async get (name) { |
|||
return this.dtp.redis.get(name); |
|||
} |
|||
|
|||
async setObject (name, value) { |
|||
return this.dtp.redis.set(name, JSON.stringify(value)); |
|||
} |
|||
|
|||
async setObjectEx (name, seconds, value) { |
|||
return this.dtp.redis.setex(name, seconds, JSON.stringify(value)); |
|||
} |
|||
|
|||
async getObject (name) { |
|||
const value = await this.dtp.redis.get(name); |
|||
if (!value) { |
|||
return; // undefined
|
|||
} |
|||
return JSON.parse(value); |
|||
} |
|||
|
|||
async hashSet (name, field, value) { |
|||
return this.dtp.redis.hset(name, field, value); |
|||
} |
|||
|
|||
async hashInc (name, field, value) { |
|||
return this.dtp.redis.hincrby(name, field, value); |
|||
} |
|||
|
|||
async hashIncFloat (name, field, value) { |
|||
return this.dtp.redis.hincrbyfloat(name, field, value); |
|||
} |
|||
|
|||
async hashGet (name, field) { |
|||
return this.dtp.redis.hget(name, field); |
|||
} |
|||
|
|||
async hashGetAll (name) { |
|||
return this.dtp.redis.hgetall(name); |
|||
} |
|||
|
|||
async hashDelete (name, field) { |
|||
return this.dtp.redis.hdel(name, field); |
|||
} |
|||
|
|||
async del (name) { |
|||
return this.dtp.redis.del(name); |
|||
} |
|||
|
|||
getKeys (pattern) { |
|||
return new Promise((resolve, reject) => { |
|||
return this.dtp.redis.keys(pattern, (err, response) => { |
|||
if (err) { |
|||
return reject(err); |
|||
} |
|||
return resolve(response); |
|||
}); |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,61 @@ |
|||
// crypto.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import crypto from 'node:crypto'; |
|||
|
|||
import { SiteService } from '../../lib/site-lib.js'; |
|||
|
|||
export default class CryptoService extends SiteService { |
|||
|
|||
static get slug () { return 'crypto'; } |
|||
static get name ( ) { return 'CryptoService'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, CryptoService); |
|||
} |
|||
|
|||
maskPassword (passwordSalt, password) { |
|||
const hash = crypto.createHash('sha256'); |
|||
|
|||
hash.update(process.env.DTP_PASSWORD_SALT); |
|||
hash.update(passwordSalt); |
|||
hash.update(password); |
|||
|
|||
return hash.digest('hex'); |
|||
} |
|||
|
|||
createHash (content, algorithm = 'sha256') { |
|||
const hash = crypto.createHash(algorithm); |
|||
hash.update(content); |
|||
return hash.digest('hex'); |
|||
} |
|||
|
|||
hash32 (text) { |
|||
var hash = 0, i, chr; |
|||
if (text.length === 0) { |
|||
return hash; |
|||
} |
|||
for (i = text.length - 1; i >= 0; --i) { |
|||
chr = text.charCodeAt(i); |
|||
// jshint ignore:start
|
|||
hash = ((hash << 5) - hash) + chr; |
|||
hash |= 0; |
|||
// jshint ignore:end
|
|||
} |
|||
hash = hash.toString(16); |
|||
if (hash[0] === '-') { |
|||
hash = hash.slice(1); |
|||
} |
|||
return hash; |
|||
} |
|||
|
|||
createProof (secret, challenge) { |
|||
let hash = crypto.createHash('sha256'); |
|||
hash.update(secret); |
|||
hash.update(challenge); |
|||
return hash.digest('hex'); |
|||
} |
|||
} |
@ -0,0 +1,315 @@ |
|||
// email.js
|
|||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
import nodemailer from 'nodemailer'; |
|||
import { v4 as uuidv4 } from 'uuid'; |
|||
|
|||
import mongoose from 'mongoose'; |
|||
|
|||
const EmailBlacklist = mongoose.model('EmailBlacklist'); |
|||
const EmailVerify = mongoose.model('EmailVerify'); |
|||
const EmailLog = mongoose.model('EmailLog'); |
|||
|
|||
import disposableEmailDomains from 'disposable-email-provider-domains'; |
|||
import emailValidator from 'email-validator'; |
|||
import emailDomainCheck from 'email-domain-check'; |
|||
|
|||
import moment from 'moment'; |
|||
import numeral from 'numeral'; |
|||
|
|||
import { SiteService, SiteError } from '../../lib/site-lib.js'; |
|||
|
|||
export default class EmailService extends SiteService { |
|||
|
|||
static get slug () { return 'email'; } |
|||
static get name ( ) { return 'EmailService'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, EmailService); |
|||
this.populateEmailVerify = [ |
|||
{ |
|||
path: 'user', |
|||
select: '_id email username username_lc displayName picture', |
|||
}, |
|||
]; |
|||
} |
|||
|
|||
async start ( ) { |
|||
await super.start(); |
|||
const { jobQueue: jobQueueService } = this.dtp.services; |
|||
|
|||
if (process.env.DTP_EMAIL_SERVICE !== 'enabled') { |
|||
this.log.info("DTP_EMAIL_SERVICE is disabled, the system can't send email and will not try."); |
|||
return; |
|||
} |
|||
|
|||
const SMTP_PORT = parseInt(process.env.DTP_EMAIL_SMTP_PORT || '587', 10); |
|||
this.log.info('creating SMTP transport', { |
|||
host: process.env.DTP_EMAIL_SMTP_HOST, |
|||
port: SMTP_PORT, |
|||
}); |
|||
this.transport = nodemailer.createTransport({ |
|||
host: process.env.DTP_EMAIL_SMTP_HOST, |
|||
port: SMTP_PORT, |
|||
secure: process.env.DTP_EMAIL_SMTP_SECURE === 'enabled', |
|||
auth: { |
|||
user: process.env.DTP_EMAIL_SMTP_USER, |
|||
pass: process.env.DTP_EMAIL_SMTP_PASS, |
|||
}, |
|||
pool: true, |
|||
maxConnections: parseInt(process.env.DTP_EMAIL_SMTP_POOL_MAX_CONN || '5', 10), |
|||
maxMessages: parseInt(process.env.DTP_EMAIL_SMTP_POOL_MAX_MSGS || '5', 10), |
|||
}); |
|||
|
|||
this.templates = { |
|||
html: { |
|||
welcome: this.loadAppTemplate('html', 'welcome.pug'), |
|||
passwordReset: this.loadAppTemplate('html', 'password-reset.pug'), |
|||
marketingBlast: this.loadAppTemplate('html', 'marketing-blast.pug'), |
|||
userEmail: this.loadAppTemplate('html', 'user-email.pug'), |
|||
}, |
|||
|
|||
text: { |
|||
welcome: this.loadAppTemplate('text', 'welcome.pug'), |
|||
passwordReset: this.loadAppTemplate('text', 'password-reset.pug'), |
|||
marketingBlast: this.loadAppTemplate('text', 'marketing-blast.pug'), |
|||
userEmail: this.loadAppTemplate('text', 'user-email.pug'), |
|||
}, |
|||
}; |
|||
|
|||
this.emailQueue = jobQueueService.getJobQueue( |
|||
'email', |
|||
this.dtp.config.jobQueues.email, |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Renders a configured email template using the specified template view model |
|||
* to produce the requested output content type (HTML or text). |
|||
* |
|||
* @param {String} templateId the identifier of the template to use |
|||
* @param {*} templateType Either 'html' or 'text' to indicate the type of |
|||
* output desired. |
|||
* @param {*} templateModel The template's view model from which template |
|||
* data will be inserted into the template being rendered. |
|||
* @returns the resulting rendered content (HTML or text as requested). |
|||
*/ |
|||
async renderTemplate (templateId, templateType, templateModel) { |
|||
const { cache: cacheService } = this.dtp.services; |
|||
const { config } = this.dtp; |
|||
|
|||
const settingsKey = `settings:${config.site.domainKey}:site`; |
|||
const adminSettings = await cacheService.getObject(settingsKey); |
|||
|
|||
templateModel.site = Object.assign({ }, config.site); // defaults and .env
|
|||
templateModel.site = Object.assign(templateModel.site, adminSettings); // admin overrides
|
|||
|
|||
// this.log.debug('rendering email template', { templateId, templateType });
|
|||
return this.templates[templateType][templateId](templateModel); |
|||
} |
|||
|
|||
/** |
|||
* Sends the email message using the configured NodeMailer transport. |
|||
* The message specifies to, from, subject, html, and text content. |
|||
* @param {NodeMailerMessage} message the message to be sent. |
|||
*/ |
|||
async send (message) { |
|||
const NOW = new Date(); |
|||
|
|||
this.log.info('sending email', { to: message.to, subject: message.subject }); |
|||
const response = await this.transport.sendMail(message); |
|||
|
|||
const log = await EmailLog.create({ |
|||
created: NOW, |
|||
from: message.from.address || message.from, |
|||
to: message.to.address || message.to, |
|||
to_lc: (message.to.address || message.to).toLowerCase(), |
|||
subject: message.subject, |
|||
messageId: response.messageId, |
|||
}); |
|||
|
|||
return log.toObject(); |
|||
} |
|||
|
|||
async sendWelcomeEmail (user) { |
|||
/* |
|||
* Remove all pending EmailVerify tokens for the User. |
|||
*/ |
|||
await this.removeVerificationTokensForUser(user); |
|||
|
|||
/* |
|||
* Create the new/only EmailVerify token for the user. This will be the only |
|||
* token accepted. Previous emails sent (if they were received) are invalid |
|||
* after this. |
|||
*/ |
|||
const verifyToken = await this.createVerificationToken(user); |
|||
|
|||
/* |
|||
* Send the welcome email using the new EmailVerify token so it can |
|||
* construct a new, valid link to use for verifying the email address. |
|||
*/ |
|||
const templateModel = { |
|||
site: this.dtp.config.site, |
|||
recipient: user, |
|||
emailVerifyToken: verifyToken.token, |
|||
}; |
|||
const message = { |
|||
from: process.env.DTP_EMAIL_SMTP_FROM, |
|||
to: user.email, |
|||
subject: `Welcome to ${this.dtp.config.site.name}!`, |
|||
html: await this.renderTemplate('welcome', 'html', templateModel), |
|||
text: await this.renderTemplate('welcome', 'text', templateModel), |
|||
}; |
|||
await this.send(message); |
|||
} |
|||
|
|||
async sendMassMail (definition) { |
|||
this.log.info('mass mail definition', { definition }); |
|||
|
|||
await this.emailQueue.add('marketing-email', { |
|||
subject: definition.subject, |
|||
textMessage: definition.textMessage, |
|||
htmlMessage: definition.htmlMessage, |
|||
}); |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
/** |
|||
* Checks an email address for validity and to not be blocked or blacklisted |
|||
* by the service. Does not return a Boolean. Will instead throw an error if |
|||
* the address is rejected by the system. |
|||
* @param {String} emailAddress The email address to be checked. |
|||
*/ |
|||
async checkEmailAddress (emailAddress) { |
|||
this.log.debug('validating email address', { emailAddress }); |
|||
if (!emailValidator.validate(emailAddress)) { |
|||
throw new Error('Email address is invalid'); |
|||
} |
|||
|
|||
const domainCheck = await emailDomainCheck(emailAddress); |
|||
this.log.debug('email domain check', { domainCheck }); |
|||
if (!domainCheck) { |
|||
throw new Error('Email address is invalid'); |
|||
} |
|||
|
|||
await this.isEmailBlacklisted(emailAddress); |
|||
} |
|||
|
|||
/** |
|||
* Check if the provided email address is blacklisted either by our database |
|||
* of known-malicious/spammy emails or by this system itself. |
|||
* @param {String} emailAddress The email address to be checked |
|||
* @returns true if the address is blacklisted; false otherwise. |
|||
*/ |
|||
async isEmailBlacklisted (emailAddress) { |
|||
emailAddress = emailAddress.toLowerCase().trim(); |
|||
|
|||
const domain = emailAddress.split('@')[1]; |
|||
this.log.debug('checking email domain for blacklist', { domain }); |
|||
if (disposableEmailDomains.domains.includes(domain)) { |
|||
this.log.alert('blacklisted email domain blocked', { emailAddress, domain }); |
|||
throw new Error('Invalid email address'); |
|||
} |
|||
|
|||
const blacklistRecord = await EmailBlacklist.findOne({ email: emailAddress }); |
|||
if (blacklistRecord) { |
|||
throw new Error('Email address has requested to not receive emails', { blacklistRecord }); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Creates an email verification token that can be used to verify the email |
|||
* address of a new site member. |
|||
* @param {User} user the user for which an email verification token should |
|||
* be created. |
|||
* @returns new email verification token (for use in creating a link to it) |
|||
*/ |
|||
async createVerificationToken (user) { |
|||
const NOW = new Date(); |
|||
const verify = new EmailVerify(); |
|||
|
|||
verify.created = NOW; |
|||
verify.user = user._id; |
|||
verify.token = uuidv4(); |
|||
|
|||
await verify.save(); |
|||
|
|||
this.log.info('created email verification token for user', { user: user._id }); |
|||
|
|||
return verify.toObject(); |
|||
} |
|||
|
|||
async getVerificationToken (token) { |
|||
const emailVerify = await EmailVerify |
|||
.findOne({ token }) |
|||
.populate(this.populateEmailVerify) |
|||
.lean(); |
|||
return emailVerify; |
|||
} |
|||
|
|||
/** |
|||
* Checks an email verification token for validity, and updates the associated |
|||
* User's isEmailVerified flag on success. |
|||
* @param {String} token The token received from the email verification |
|||
* request. |
|||
*/ |
|||
async verifyToken (token) { |
|||
const NOW = new Date(); |
|||
const { user: userService } = this.dtp.services; |
|||
|
|||
// fetch the token from the db
|
|||
const emailVerify = await EmailVerify |
|||
.findOne({ token: token }) |
|||
.populate(this.populateEmailVerify) |
|||
.lean(); |
|||
|
|||
// verify that the token is at least valid (it exists)
|
|||
if (!emailVerify) { |
|||
this.log.error('email verify token not found', { token }); |
|||
throw new SiteError(403, 'Email verification token is invalid'); |
|||
} |
|||
|
|||
// verify that it hasn't already been verified (user clicked link more than once)
|
|||
if (emailVerify.verified) { |
|||
this.log.error('email verify token already claimed', { token }); |
|||
throw new SiteError(403, 'Email verification token is invalid'); |
|||
} |
|||
|
|||
this.log.info('marking user email verified', { userId: emailVerify.user._id }); |
|||
await userService.setEmailVerification(emailVerify.user, true); |
|||
|
|||
await EmailVerify.updateOne({ _id: emailVerify._id }, { $set: { verified: NOW } }); |
|||
} |
|||
|
|||
/** |
|||
* Removes all pending EmailVerify tokens for the specified user. This will |
|||
* happen when the User changes their email address or if triggered |
|||
* administratively on the User account. |
|||
* |
|||
* @param {User} user the User for whom all pending verification tokens are |
|||
* to be removed. |
|||
*/ |
|||
async removeVerificationTokensForUser (user) { |
|||
this.log.info('removing all pending email address verification tokens for user', { user: user._id }); |
|||
await EmailVerify.deleteMany({ user: user._id }); |
|||
} |
|||
|
|||
createMessageModel (viewModel) { |
|||
const messageModel = { |
|||
process: { |
|||
env: process.env, |
|||
}, |
|||
config: this.dtp.config, |
|||
site: this.dtp.config.site, |
|||
moment, |
|||
numeral, |
|||
}; |
|||
return Object.assign(messageModel, viewModel); |
|||
} |
|||
} |
@ -0,0 +1,64 @@ |
|||
// job-queue.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import Bull from 'bull'; |
|||
|
|||
import { SiteService } from '../../lib/site-lib.js'; |
|||
|
|||
export default class JobQueueService extends SiteService { |
|||
|
|||
static get slug () { return 'jobQueue'; } |
|||
static get name ( ) { return 'JobQueueService'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, JobQueueService); |
|||
this.queues = { }; |
|||
} |
|||
|
|||
getJobQueue (name, defaultJobOptions) { |
|||
/* |
|||
* If we have a named queue, return it. |
|||
*/ |
|||
let queue = this.queues[name]; |
|||
if (queue) { |
|||
return queue; |
|||
} |
|||
|
|||
/* |
|||
* Create a new named queue |
|||
*/ |
|||
defaultJobOptions = Object.assign({ |
|||
priority: 10, |
|||
delay: 0, |
|||
attempts: 1, |
|||
removeOnComplete: true, |
|||
removeOnFail: false, |
|||
}, defaultJobOptions); |
|||
|
|||
queue = new Bull(name, { |
|||
prefix: process.env.REDIS_KEY_PREFIX || 'dtp', |
|||
redis: { |
|||
host: process.env.REDIS_HOST, |
|||
port: parseInt(process.env.REDIS_PORT || '6379', 10), |
|||
password: process.env.REDIS_PASSWORD, |
|||
}, |
|||
defaultJobOptions, |
|||
}); |
|||
queue.setMaxListeners(64); |
|||
this.queues[name] = queue; |
|||
|
|||
return queue; |
|||
} |
|||
|
|||
async discoverJobQueues (pattern) { |
|||
const { cache: cacheService } = this.dtp.services; |
|||
let bullQueues = await cacheService.getKeys(pattern); |
|||
return bullQueues |
|||
.map((queue) => queue.split(':')[1]) |
|||
.sort() |
|||
; |
|||
} |
|||
} |
@ -0,0 +1,77 @@ |
|||
// edit-with-vi.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
// Based on:
|
|||
// https://github.com/voidful/text-filtering-js/blob/master/text_filtering.js
|
|||
// - Does not extend String because stop it.
|
|||
// - CommonJS module
|
|||
|
|||
'use strict'; |
|||
|
|||
/* |
|||
* This file must only be edited with vi/vim. If you so much as *open* this file |
|||
* in VSCode, you've probably damaged the file. Do not save it. Just close it, |
|||
* and go edit the file with vi or vim. |
|||
* |
|||
* VS Code, being web-based, contains logic to filter out the content used to |
|||
* implement the filter. You will erase that content, and then various attackers |
|||
* will own your chat. |
|||
* |
|||
* If attackers have owned your chat, you may want to revert or otherwise restore |
|||
* this file to it's original state. |
|||
*/ |
|||
|
|||
export function filterBBcode (text) { |
|||
return text.replace(/\[.*\]/g, ''); |
|||
} |
|||
|
|||
export function filterLineBreak (text) { |
|||
return text.replace(/(\r\n|\n|\r)/gm," "); |
|||
} |
|||
|
|||
export function filterSmileysCode (text) { |
|||
return text |
|||
.replace(/:\$?.*:\$?/g, '') |
|||
.replace(/:\w+:?/g, '') |
|||
.replace(/:\w+/g, '') |
|||
.replace(/&#.*;/g, '') |
|||
; |
|||
} |
|||
|
|||
export function filterGuff (text) { |
|||
return text.replace('*** 作者被禁止或刪除 內容自動屏蔽 ***', ''); |
|||
} |
|||
|
|||
export function filterHtml (text) { |
|||
return text.replace(/(<[^>]*>)/g,' '); |
|||
} |
|||
|
|||
export function filterNonsense (text) { |
|||
// edited to allow CR and LF
|
|||
// text = text.replace(/[\u0000-\u001F\u007f\u00AD\u200B-\u200D\u3000\uFEFF]/g,'');
|
|||
text = text.replace(/[\u0000-\u0009\u000b\u000c\u000e\u007f\u00AD\u200B-\u200D\u3000\uFEFF]/g,''); |
|||
|
|||
text = text.replace(/\u00AD/,' '); |
|||
text = text.replace(/\u2013/,'-'); |
|||
return text; |
|||
} |
|||
|
|||
export function filterAll (text) { |
|||
text = module.exports.filterSmileysCode(text); |
|||
text = module.exports.filterBBcode(text); |
|||
text = module.exports.filterGuff(text); |
|||
text = module.exports.filterHtml(text); |
|||
text = module.exports.filterLineBreak(text); |
|||
return text; |
|||
} |
|||
|
|||
export default { |
|||
filterBBcode, |
|||
filterLineBreak, |
|||
filterSmileysCode, |
|||
filterGuff, |
|||
filterHtml, |
|||
filterNonsense, |
|||
filterAll, |
|||
}; |
@ -0,0 +1,64 @@ |
|||
// limiter.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import path from 'node:path'; |
|||
import expressLimiter from 'express-limiter'; |
|||
|
|||
import { SiteService, SiteError } from '../../lib/site-lib.js'; |
|||
|
|||
export default class LimiterService extends SiteService { |
|||
|
|||
static get slug ( ) { return 'limiter'; } |
|||
static get name ( ) { return 'LimiterService'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, LimiterService); |
|||
|
|||
this.handlers = { |
|||
lookup: this.limiterLookup.bind(this), |
|||
whitelist: this.limiterWhitelist.bind(this), |
|||
}; |
|||
} |
|||
|
|||
async start ( ) { |
|||
this.config = (await import(path.resolve(this.dtp.config.root, 'config', 'limiter.js'))).default; |
|||
this.limiter = expressLimiter(this.dtp.app, this.dtp.redis); |
|||
} |
|||
|
|||
create (config) { |
|||
const options = { |
|||
total: config.total, |
|||
expire: config.expire, |
|||
lookup: this.handlers.lookup, |
|||
whitelist: this.handlers.whitelist, |
|||
onRateLimited: async (req, res, next) => { |
|||
this.emit('limiter:block', req); |
|||
next(new SiteError(config.status || 429, config.message || 'Rate limit exceeded')); |
|||
}, |
|||
}; |
|||
// this.log.debug('creating rate limiter', { options });
|
|||
const middleware = this.limiter(options); |
|||
return async (req, res, next) => { |
|||
return middleware(req, res, next); |
|||
}; |
|||
} |
|||
|
|||
limiterLookup (req, res, options, next) { |
|||
if (req.user) { |
|||
options.lookup = 'user._id'; // req.user._id, populated by PassportJS session
|
|||
} else { |
|||
options.lookup = 'ip'; // req.ip, populated by ExpressJS with trust_proxy=1
|
|||
} |
|||
return next(); |
|||
} |
|||
|
|||
limiterWhitelist (req) { |
|||
if ((process.env.NODE_ENV === 'local') && (process.env.DTP_RATE_LIMITER === 'disabled')) { |
|||
return true; |
|||
} |
|||
return req.user && req.user.flags.isAdmin; |
|||
} |
|||
} |
@ -0,0 +1,98 @@ |
|||
// session.js
|
|||
// Copyright (C) 2022,2023 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import util from 'node:util'; |
|||
import passport from 'passport'; |
|||
|
|||
import { SiteService, SiteError } from '../../lib/site-lib.js'; |
|||
|
|||
export default class SessionService extends SiteService { |
|||
|
|||
static get slug () { return 'session'; } |
|||
static get name ( ) { return 'SessionService'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, SessionService); |
|||
} |
|||
|
|||
async start ( ) { |
|||
await super.start(); |
|||
|
|||
passport.serializeUser(this.serializeUser.bind(this)); |
|||
passport.deserializeUser(this.deserializeUser.bind(this)); |
|||
} |
|||
|
|||
async stop ( ) { |
|||
this.log.info(`stopping ${SessionService.name} service`); |
|||
} |
|||
|
|||
middleware ( ) { |
|||
return async (req, res, next) => { |
|||
res.locals.user = req.user; |
|||
res.locals.query = req.query; |
|||
|
|||
if (req.user) { |
|||
if (req.user.flags.isAdmin) { |
|||
res.locals.config = this.dtp.config; |
|||
res.locals.session = req.session; |
|||
res.locals.util = util; |
|||
} |
|||
} |
|||
|
|||
return next(); |
|||
}; |
|||
} |
|||
|
|||
authCheckMiddleware (options) { |
|||
const { membership: membershipService } = this.dtp.services; |
|||
options = Object.assign({ |
|||
requireLogin: true, |
|||
requireEmailVerified: false, |
|||
requireMembership: false, |
|||
requireModerator: false, |
|||
requireAdmin: false, |
|||
}, options); |
|||
return async (req, res, next) => { |
|||
if (options.requireLogin && !req.user) { |
|||
return next(new SiteError(403, 'Login required')); |
|||
} |
|||
if (options.requireEmailVerified && (!req.user || !req.user.flags.isEmailVerified)) { |
|||
return next(new SiteError(403, `Must verify your email address to continue. Please check your spam folder for a welcome email from ${this.dtp.config.site.name}`)); |
|||
} |
|||
if (options.requireMembership) { |
|||
res.locals.membership = await membershipService.getForUser(req.user); |
|||
if (!res.locals.membership) { |
|||
return next(new SiteError(403, 'Membership required')); |
|||
} |
|||
} |
|||
if (options.requireModerator && (!req.user || !req.user.flags.isModerator)) { |
|||
return next(new SiteError(403, 'Platform moderator privileges are required')); |
|||
} |
|||
if (options.requireAdmin && (!req.user || !req.user.flags.isAdmin)) { |
|||
return next(new SiteError(403, 'Platform administrator privileges are required')); |
|||
} |
|||
return next(); |
|||
}; |
|||
} |
|||
|
|||
async serializeUser (user, done) { |
|||
return done(null, user._id); |
|||
} |
|||
|
|||
async deserializeUser (userId, done) { |
|||
const { user: userService } = this.dtp.services; |
|||
try { |
|||
const user = await userService.getUserAccount(userId); |
|||
if (user.permissions && !user.permissions.canLogin) { |
|||
return done(null, null); // destroys user session without error
|
|||
} |
|||
return done(null, user); |
|||
} catch (error) { |
|||
this.log.error('failed to deserialize user from session', { error }); |
|||
return done(null, null); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,71 @@ |
|||
// text.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import striptags from 'striptags'; |
|||
import unzalgo from 'unzalgo'; |
|||
import shoetest from 'shoetest'; |
|||
import diacritics from 'diacritics'; |
|||
import DtpTextFilter from './lib/edit-with-vi.js'; |
|||
|
|||
import { SiteService, SiteError } from '../../lib/site-lib.js'; |
|||
|
|||
export default class TextService extends SiteService { |
|||
|
|||
static get slug () { return 'text'; } |
|||
static get name ( ) { return 'TextService'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, TextService); |
|||
} |
|||
|
|||
/** |
|||
* Basic text cleaning function to remove Zalgo and tags. |
|||
* @param {String} text The text to be cleaned |
|||
* @returns The cleaned text |
|||
*/ |
|||
clean (text) { |
|||
text = unzalgo.clean(text); |
|||
text = striptags(text.trim()); |
|||
return text; |
|||
} |
|||
|
|||
/** |
|||
* The heavy hammer of text filtering that removes all malicious and annoying |
|||
* things I know about as of this writing. Zalgo, tags, shoetest, diacritics, |
|||
* and our own custom nonsense UTF-8 and Unicode filters. |
|||
* |
|||
* This filter is very heavy-handed and merciless. |
|||
* |
|||
* @param {String} text The text to be filtered |
|||
* @returns The filtered text |
|||
*/ |
|||
filter (text) { |
|||
if (!text || (typeof text !== 'string') || (text.length < 1)) { |
|||
return text; |
|||
} |
|||
|
|||
text = DtpTextFilter.filterNonsense(text); |
|||
text = DtpTextFilter.filterGuff(text); |
|||
text = DtpTextFilter.filterHtml(text); |
|||
|
|||
text = shoetest.simplify(text); |
|||
text = diacritics.remove(text); |
|||
|
|||
for (const filter of this.chatFilters) { |
|||
const regex = new RegExp(filter, 'gi'); |
|||
if (text.match(regex)) { |
|||
this.log.alert('chat filter text match', { filter }); |
|||
throw new SiteError(403, 'Text input rejected'); |
|||
} |
|||
} |
|||
|
|||
/* |
|||
* Once all the stupidity has been stripped, strip the HTML |
|||
* tags that might remain. |
|||
*/ |
|||
return this.clean(text); |
|||
} |
|||
} |
@ -0,0 +1,664 @@ |
|||
// user.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
import path from 'node:path'; |
|||
|
|||
import mongoose from 'mongoose'; |
|||
const User = mongoose.model('User'); |
|||
|
|||
import passport from 'passport'; |
|||
import PassportLocal from 'passport-local'; |
|||
|
|||
import { v4 as uuidv4 } from 'uuid'; |
|||
|
|||
import { SiteService, SiteError } from '../../lib/site-lib.js'; |
|||
|
|||
export default class UserService extends SiteService { |
|||
|
|||
static get slug () { return 'user'; } |
|||
static get name ( ) { return 'UserService'; } |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, UserService); |
|||
|
|||
this.USER_SELECT = '_id created username username_lc displayName picture flags permissions'; |
|||
this.populateUser = [ |
|||
{ |
|||
path: 'picture.large', |
|||
}, |
|||
{ |
|||
path: 'picture.small', |
|||
}, |
|||
]; |
|||
} |
|||
|
|||
async start ( ) { |
|||
await super.start(); |
|||
this.reservedNames = await import(path.join(this.dtp.config.root, 'config', 'reserved-names.js')); |
|||
} |
|||
|
|||
async create (userDefinition) { |
|||
const NOW = new Date(); |
|||
const { |
|||
crypto: cryptoService, |
|||
email: emailService, |
|||
text: textService, |
|||
} = this.dtp.services; |
|||
|
|||
try { |
|||
userDefinition.email = userDefinition.email.trim().toLowerCase(); |
|||
|
|||
// strip characters we don't want to allow in username
|
|||
userDefinition.username = await this.filterUsername(userDefinition.username); |
|||
const username_lc = userDefinition.username.toLowerCase(); |
|||
|
|||
// test the email address for validity, blacklisting, etc.
|
|||
await emailService.checkEmailAddress(userDefinition.email); |
|||
|
|||
// test if we already have a user with this email address
|
|||
let user = await User.findOne({ |
|||
$or: [ |
|||
{ 'email': userDefinition.email }, |
|||
{ username_lc }, |
|||
], |
|||
}).lean(); |
|||
if (user) { |
|||
throw new SiteError(400, 'That account is not available for use'); |
|||
} |
|||
|
|||
user = new User(); |
|||
user.created = NOW; |
|||
|
|||
user.email = userDefinition.email; |
|||
user.username = userDefinition.username; |
|||
user.username_lc = username_lc; |
|||
user.displayName = textService.clean(userDefinition.displayName || userDefinition.username); |
|||
|
|||
user.passwordSalt = uuidv4(); |
|||
user.password = cryptoService.maskPassword(user.passwordSalt, userDefinition.password); |
|||
|
|||
this.log.debug('creating new user account', { user: user.toObject() }); |
|||
this.log.info('creating new user account', { email: userDefinition.email }); |
|||
await user.save(); |
|||
|
|||
await emailService.sendWelcomeEmail(user); |
|||
|
|||
return user.toObject(); |
|||
} catch (error) { |
|||
this.log.error('failed to create user', { error }); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
async filterUsername (username, options) { |
|||
const { text: textService } = this.dtp.services; |
|||
|
|||
options = Object.assign({ checkReserved: true }, options); |
|||
if (!username || (username.length < 1)) { |
|||
throw new SiteError(400, 'Username must not be blank'); |
|||
} |
|||
|
|||
username = textService.clean(username); |
|||
username = username.replace(/[^A-Za-z0-9\-_]/gi, ''); |
|||
|
|||
if (options.checkReserved && (await this.isUsernameReserved(username))) { |
|||
throw new SiteError(403, 'The username you entered is not available for use.'); |
|||
} |
|||
|
|||
return username; |
|||
} |
|||
|
|||
async lookup (account, options) { |
|||
options = Object.assign({ |
|||
withEmail: false, |
|||
withCredentials: false, |
|||
}, options); |
|||
|
|||
this.log.debug('locating user record', { account }); |
|||
|
|||
const selects = [ |
|||
'_id', 'created', |
|||
'username', 'username_lc', |
|||
'displayName', 'picture', |
|||
'flags', 'permissions', |
|||
]; |
|||
if (options.withEmail) { |
|||
selects.push('email'); |
|||
} |
|||
if (options.withCredentials) { |
|||
selects.push('passwordSalt'); |
|||
selects.push('password'); |
|||
} |
|||
|
|||
const usernameRegex = new RegExp(`^${account}.*`); |
|||
const user = await User |
|||
.findOne({ |
|||
$or: [ |
|||
{ email: account.email }, |
|||
{ username_lc: usernameRegex }, |
|||
] |
|||
}) |
|||
.select(selects.join(' ')) |
|||
.lean(); |
|||
|
|||
return user; |
|||
} |
|||
|
|||
async autocomplete (pagination, username) { |
|||
if (!username || (username.length === 0)) { |
|||
throw new SiteError(406, "Username must not be empty"); |
|||
} |
|||
this.log.debug('autocompleting username partial', { username }); |
|||
let search = { |
|||
username_lc: new RegExp(username.toLowerCase().trim(), 'gi'), |
|||
}; |
|||
const users = await User |
|||
.find(search) |
|||
.sort({ username_lc: 1 }) |
|||
.select({ username: 1, username_lc: 1 }) |
|||
.skip(pagination.skip) |
|||
.limit(pagination.cpp) |
|||
.lean() |
|||
; |
|||
return users.map((u) => { |
|||
return { |
|||
username: u.username, |
|||
username_lc: u.username_lc, |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
async getByUsername (username, options) { |
|||
options = options || { }; |
|||
username = username.trim().toLowerCase(); |
|||
|
|||
let select = ['+flags', '+permissions']; |
|||
const user = await User |
|||
.findOne({ username_lc: username }) |
|||
.select(select.join(' ')) |
|||
.populate(this.populateUser) |
|||
.lean(); |
|||
|
|||
return user; |
|||
} |
|||
|
|||
async getUserAccount (userId) { |
|||
const user = await User |
|||
.findById(userId) |
|||
.select('+email +flags +flags.isCloaked +permissions +optIn +paymentTokens.handcash +paymentTokens.stripe') |
|||
.populate([ |
|||
{ |
|||
path: 'picture.large', |
|||
}, |
|||
{ |
|||
path: 'picture.small', |
|||
}, |
|||
{ |
|||
path: 'membership', |
|||
populate: [ |
|||
{ |
|||
path: 'plan', |
|||
}, |
|||
], |
|||
}, |
|||
]) |
|||
.lean(); |
|||
|
|||
if (!user) { |
|||
throw new SiteError(404, 'Member account not found'); |
|||
} |
|||
|
|||
return user; |
|||
} |
|||
|
|||
async getUserAccounts (pagination, username) { |
|||
let search = { }; |
|||
if (username) { |
|||
search.$or = [ |
|||
{ $text: { $search: username } }, |
|||
{ username_lc: new RegExp(username.toLowerCase().trim(), 'gi') }, |
|||
]; |
|||
} |
|||
|
|||
const users = await User |
|||
.find(search) |
|||
.sort({ username_lc: 1 }) |
|||
.select('+email +flags +permissions, +optIn') |
|||
.skip(pagination.skip) |
|||
.limit(pagination.cpp) |
|||
.lean() |
|||
; |
|||
return users; |
|||
} |
|||
|
|||
async isUsernameReserved (username) { |
|||
if (this.reservedNames.includes(username)) { |
|||
this.log.alert('prohibiting use of reserved username', { username }); |
|||
return true; |
|||
} |
|||
|
|||
const user = await User.findOne({ username: username}).select('username').lean(); |
|||
if (user) { |
|||
this.log.alert('username is already registered', { username }); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
async setEmailVerification (user, isVerified) { |
|||
await User.updateOne( |
|||
{ _id: user._id }, |
|||
{ |
|||
$set: { 'flags.isEmailVerified': isVerified }, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
async emailOptOut (userId, category) { |
|||
userId = mongoose.Types.ObjectId(userId); |
|||
const user = await this.getUserAccount(userId); |
|||
if (!user) { |
|||
throw new SiteError(406, 'Invalid opt-out token'); |
|||
} |
|||
|
|||
const updateOp = { $set: { } }; |
|||
switch (category) { |
|||
case 'marketing': |
|||
updateOp.$set['optIn.marketing'] = false; |
|||
break; |
|||
case 'system': |
|||
updateOp.$set['optIn.system'] = false; |
|||
break; |
|||
default: |
|||
throw new SiteError(406, 'Invalid opt-out category'); |
|||
} |
|||
|
|||
await User.updateOne({ _id: userId }, updateOp); |
|||
} |
|||
|
|||
async requestPasswordReset (usernameOrEmail) { |
|||
const { authToken: authTokenService, email: emailService } = this.dtp.services; |
|||
const { site } = this.dtp.config; |
|||
|
|||
const lookupAccount = { |
|||
email: usernameOrEmail.trim().toLowerCase(), |
|||
username: (await this.filterUsername(usernameOrEmail)).toLowerCase(), |
|||
}; |
|||
const recipient = await this.lookup(lookupAccount, { withEmail: true }); |
|||
if (!recipient) { |
|||
throw new SiteError(404, 'User account not found'); |
|||
} |
|||
|
|||
/* |
|||
* Generate a password reset email and send it to the user. They will have |
|||
* to click a link in the email with a token on it that brings them to the |
|||
* password reset view to enter a new password with a fresh reminder to |
|||
* write it down and store that in a safe place. |
|||
*/ |
|||
|
|||
const passwordResetToken = await authTokenService.create('password-reset', { |
|||
_id: recipient._id, |
|||
username: recipient.username, |
|||
}); |
|||
|
|||
const templateModel = { |
|||
site, |
|||
messageType: 'system', |
|||
recipient, |
|||
passwordResetToken, |
|||
}; |
|||
const message = { |
|||
from: process.env.DTP_EMAIL_SMTP_FROM, |
|||
to: recipient.email, |
|||
subject: `Password reset request for ${recipient.username_lc}@${this.dtp.config.site.domain}!`, |
|||
html: await emailService.renderTemplate('passwordReset', 'html', templateModel), |
|||
text: await emailService.renderTemplate('passwordReset', 'text', templateModel), |
|||
}; |
|||
|
|||
this.log.info('sending password-reset email', { |
|||
site: site.domain, |
|||
recipient: recipient.username, |
|||
}); |
|||
await emailService.send(message); |
|||
|
|||
return recipient; |
|||
} |
|||
|
|||
async hasPaidMembership (user) { |
|||
if (!user || !user.membership) { |
|||
return false; |
|||
} |
|||
return user.membership.tier !== 'free'; |
|||
} |
|||
|
|||
registerPassportLocal ( ) { |
|||
const options = { |
|||
usernameField: 'username', |
|||
passwordField: 'password', |
|||
session: true, |
|||
}; |
|||
passport.use('dtp-local', new PassportLocal(options, this.handleLocalLogin.bind(this))); |
|||
} |
|||
|
|||
async handleLocalLogin (username, password, done) { |
|||
const now = new Date(); |
|||
this.log.info('handleLocalLogin', { username }); |
|||
try { |
|||
if (!username || (username.length < 1)) { |
|||
throw new SiteError(403, 'Username is required'); |
|||
} |
|||
const user = await this.authenticate({ username, password }, { adminRequired: false }); |
|||
await this.startSession(user, now); |
|||
done(null, this.filterUserObject(user)); |
|||
} catch (error) { |
|||
this.log.error('failed to process local user login', { error }); |
|||
done(error); |
|||
} |
|||
} |
|||
|
|||
registerPassportAdmin ( ) { |
|||
const options = { |
|||
usernameField: 'username', |
|||
passwordField: 'password', |
|||
session: true, |
|||
}; |
|||
this.log.info('registering PassportJS admin strategy', { options }); |
|||
passport.use('dtp-admin', new PassportLocal(options, this.handleAdminLogin.bind(this))); |
|||
} |
|||
|
|||
async handleAdminLogin (email, password, done) { |
|||
const now = new Date(); |
|||
try { |
|||
const user = await this.authenticate({ email, password }, { adminRequired: true }); |
|||
await this.startSession(user, now); |
|||
done(null, this.filterUserObject(user)); |
|||
} catch (error) { |
|||
this.log.error('failed to process admin user login', { error }); |
|||
done(error); |
|||
} |
|||
} |
|||
|
|||
async login (req, res, next, options) { |
|||
options = Object.assign({ |
|||
loginUrl: '/welcome/login', |
|||
redirectUrl: '/', |
|||
}, options); |
|||
try { |
|||
passport.authenticate('dtp-local', (error, user/*, info*/) => { |
|||
if (error) { |
|||
req.session.loginResult = error.toString(); |
|||
return next(error); |
|||
} |
|||
if (!user) { |
|||
req.session.loginResult = 'Username or email address is unknown.'; |
|||
if (options.onLoginFailed) { |
|||
return options.onLoginFailed(req, res, next); |
|||
} |
|||
return res.redirect(options.loginUrl); |
|||
} |
|||
req.login(user, (error) => { |
|||
if (error) { |
|||
return next(error); |
|||
} |
|||
if (options.onLoginSuccess) { |
|||
return options.onLoginSuccess(req, res, next); |
|||
} |
|||
return res.redirect(options.redirectUrl); |
|||
}); |
|||
})(req, res, next); |
|||
} catch (error) { |
|||
this.log.error('failed to process user login', { error }); |
|||
return next(error); |
|||
} |
|||
} |
|||
|
|||
async authenticate (account, options) { |
|||
const { crypto } = this.dtp.services; |
|||
|
|||
options = Object.assign({ |
|||
adminRequired: false, |
|||
}, options); |
|||
|
|||
const user = await User |
|||
.findOne({ |
|||
$or: [ |
|||
{ email: account.username.trim().toLowerCase() }, |
|||
{ username_lc: (await this.filterUsername(account.username, { checkReserved: false })).toLowerCase() }, |
|||
] |
|||
}) |
|||
.select('_id created username username_lc displayName picture flags permissions email passwordSalt password') |
|||
.lean(); |
|||
if (!user) { |
|||
throw new SiteError(404, 'User account not found'); |
|||
} |
|||
if (options.adminRequired && !user.flags.isAdmin) { |
|||
throw new SiteError(403, 'Admin privileges required'); |
|||
} |
|||
|
|||
const maskedPassword = crypto.maskPassword( |
|||
user.passwordSalt, |
|||
account.password, |
|||
); |
|||
if (maskedPassword !== user.password) { |
|||
throw new SiteError(403, 'Account credentials do not match'); |
|||
} |
|||
|
|||
// remove these critical fields from the user object
|
|||
delete user.passwordSalt; |
|||
delete user.password; |
|||
|
|||
this.log.debug('user authenticated', { username: user.username }); |
|||
|
|||
return user; |
|||
} |
|||
|
|||
async startSession (user, now) { |
|||
await User.updateOne( |
|||
{ _id: user._id }, |
|||
{ |
|||
$set: { 'stats.lastLogin': now }, |
|||
$inc: { 'stats.loginCount': 1 }, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
async updatePassword (user, password) { |
|||
const { crypto: cryptoService } = this.dtp.services; |
|||
|
|||
const passwordSalt = uuidv4(); |
|||
const passwordHash = cryptoService.maskPassword(passwordSalt, password); |
|||
|
|||
this.log.info('updating user password', { userId: user._id }); |
|||
await User.updateOne( |
|||
{ _id: user._id }, |
|||
{ |
|||
$set: { |
|||
passwordSalt: passwordSalt, |
|||
password: passwordHash, |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
async setUserSettings (user, settings) { |
|||
const { |
|||
crypto: cryptoService, |
|||
email: emailService, |
|||
text: textService, |
|||
} = this.dtp.services; |
|||
|
|||
const update = { $set: { } }; |
|||
const actions = [ ]; |
|||
let emailChanged = false; |
|||
|
|||
if (settings.displayName && (settings.displayName !== user.displayName)) { |
|||
update.$set.displayName = textService.clean(settings.displayName).trim(); |
|||
actions.push('Display name updated'); |
|||
} |
|||
if (settings.bio && (settings.bio !== user.bio)) { |
|||
update.$set.bio = textService.filter(settings.bio).trim(); |
|||
actions.push('bio updated'); |
|||
} |
|||
|
|||
if (!settings.username) { |
|||
throw new SiteError(400, 'Username must not be empty'); |
|||
} |
|||
if (settings.username && (settings.username !== user.username)) { |
|||
update.$set.username = await this.filterUsername(settings.username); |
|||
update.$set.username_lc = update.$set.username.toLowerCase(); |
|||
} |
|||
|
|||
if (settings.email && (settings.email !== user.email)) { |
|||
settings.email = settings.email.toLowerCase().trim(); |
|||
await emailService.checkEmailAddress(settings.email); |
|||
update.$set['flags.isEmailVerified'] = false; |
|||
update.$set.email = settings.email; |
|||
actions.push('Email address updated and verification email sent. Please check your inbox and follow the instructions included to complete the change of your email address.'); |
|||
emailChanged = true; |
|||
} |
|||
|
|||
if (settings.password) { |
|||
if (settings.password !== settings.passwordv) { |
|||
throw new SiteError(400, 'Password and password verification do not match.'); |
|||
} |
|||
update.$set.passwordSalt = uuidv4(); |
|||
update.$set.password = cryptoService.maskPassword(update.$set.passwordSalt, settings.password); |
|||
actions.push('Password changed successfully.'); |
|||
} |
|||
|
|||
update.$set.optIn = { |
|||
system: settings['optIn.system'] === 'on', |
|||
newsletter: settings['optIn.newsletter'] === 'on', |
|||
}; |
|||
|
|||
if (user.flags && user.flags.isModerator) { |
|||
update.$set['flags.isCloaked'] = settings['flags.isCloaked'] === 'on'; |
|||
} |
|||
|
|||
await User.updateOne({ _id: user._id }, update); |
|||
|
|||
/* |
|||
* Re-load the User from the database, and use the updated User to send a |
|||
* new welcome email if the email address was changed. |
|||
*/ |
|||
if (emailChanged) { |
|||
user = await this.getUserAccount(user._id); |
|||
await this.sendWelcomeEmail(user); |
|||
} |
|||
|
|||
return actions; |
|||
} |
|||
|
|||
async setLastAnnouncement (user, announcement) { |
|||
await User.updateOne( |
|||
{ _id: user._id }, |
|||
{ |
|||
$set: { lastAnnouncement: announcement.created }, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
filterUserObject (user) { |
|||
return { |
|||
_id: user._id, |
|||
created: user.created, |
|||
username: user.username, |
|||
username_lc: user.username_lc, |
|||
displayName: user.displayName, |
|||
bio: user.bio, |
|||
picture: user.picture, |
|||
flags: user.flags, |
|||
permissions: user.permissions, |
|||
membership: user.membership, |
|||
stats: user.stats, |
|||
}; |
|||
} |
|||
|
|||
async banUser (user) { |
|||
const { chat: chatService } = this.dtp.services; |
|||
|
|||
const userTag = { _id: user._id, username: user.username }; |
|||
this.log.alert('banning user', userTag); |
|||
|
|||
this.log.info('removing user chat messages', userTag); |
|||
await chatService.deleteAllForUser(user); |
|||
|
|||
this.log.info('removing all user privileges', userTag); |
|||
await User.updateOne( |
|||
{ _id: user._id }, |
|||
{ |
|||
$set: { |
|||
'flags.isAdmin': false, |
|||
'flags.isModerator': false, |
|||
'flags.isEmailVerified': false, |
|||
'permissions.canLogin': false, |
|||
'permissions.canChat': false, |
|||
'permissions.canComment': false, |
|||
'permissions.canReport': false, |
|||
'permissions.canShareLinks': false, |
|||
'optIn.system': false, |
|||
'optIn.marketing': false, |
|||
badges: [ ], |
|||
favoriteStickers: [ ], |
|||
}, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
async block (user, blockedUserId) { |
|||
blockedUserId = mongoose.Types.ObjectId(blockedUserId); |
|||
this.log.info('blocking user', { user: user._id, blockedUserId }); |
|||
await User.updateOne( |
|||
{ _id: user._id }, |
|||
{ $addToSet: { blockedUsers: blockedUserId } }, |
|||
); |
|||
} |
|||
|
|||
async getBlockList (userId) { |
|||
const user = await User |
|||
.findOne({ _id: userId }) |
|||
.select('blockedUsers') |
|||
.lean(); |
|||
if (!user) { |
|||
return [ ]; |
|||
} |
|||
return user.blockedUsers || [ ]; |
|||
} |
|||
|
|||
async getBlockedUsers (userId) { |
|||
const user = await User |
|||
.findOne({ _id: userId }) |
|||
.select('blockedUsers') |
|||
.populate({ |
|||
path: 'blockedUsers', |
|||
select: this.USER_SELECT, |
|||
}) |
|||
.lean(); |
|||
if (!user) { |
|||
return [ ]; |
|||
} |
|||
return user.blockedUsers || [ ]; |
|||
} |
|||
|
|||
async unblock (user, blockedUserId) { |
|||
await User.updateOne( |
|||
{ _id: user._id }, |
|||
{ $pull: { blockedUsers: blockedUserId } }, |
|||
); |
|||
} |
|||
|
|||
async isBlocked (user, blockedUserId) { |
|||
/* |
|||
* This is faster than using countDocuments - just fetch the _id |
|||
*/ |
|||
const test = await User |
|||
.findOne({ _id: user._id, blockedUsers: blockedUserId }) |
|||
.select('_id') // ignoring all data, do they exist?
|
|||
.lean(); |
|||
return !!test; |
|||
} |
|||
} |
@ -0,0 +1,15 @@ |
|||
.common-footer |
|||
p |
|||
| This email was sent to #{recipient.email} because you selected to receive emails from #{site.name}. |
|||
if messageType !== 'system' |
|||
| You can #[a(href=`https://${site.domain}/email/opt-out?u=${recipient._id}&c=${messageType || 'marketing'}`) opt out] at any time to stop receiving these emails. |
|||
|
|||
if site.address1 && site.city && site.state && site.postalCode && site.country |
|||
p You can request to stop receiving these emails in writing at: |
|||
address |
|||
div= site.company |
|||
div= site.address1 |
|||
if site.address2 && (site.address2.length > 0) |
|||
div= site.address2 |
|||
div #{site.city}, #{site.state} #{site.postalCode} |
|||
div= site.country |
@ -0,0 +1,2 @@ |
|||
.common-title= emailTitle || `Greetings from ${site.name}!` |
|||
.common-slogan= site.description |
@ -0,0 +1,19 @@ |
|||
| |
|||
| --- |
|||
| This email was sent to #{recipient.email} because you selected to receive emails from #{site.name}. |
|||
if messageType !== 'system' |
|||
| |
|||
| Visit #{`https://${site.domain}/email/opt-out?u=${recipient._id}&c=${messageType || 'marketing'}`} to opt out and stop receiving these emails. |
|||
|
|||
if site.address1 && site.city && site.state && site.postalCode && site.country |
|||
| |
|||
| |
|||
| You can request to stop receiving these emails in writing at: |
|||
| |
|||
| #{site.company} |
|||
| #{site.address1}#{'\n'} |
|||
if site.address2 && (site.address2.length > 0) |
|||
| #{site.address2}#{'\n'} |
|||
| #{site.city}, #{site.state} #{site.postalCode} |
|||
| #{site.country} |
|||
| |
@ -0,0 +1,2 @@ |
|||
| Dear #{recipient.displayName || recipient.username}, |
|||
| |
@ -0,0 +1,3 @@ |
|||
extends ../layouts/html/system-message |
|||
block message-body |
|||
.content-message!= htmlMessage |
@ -0,0 +1,13 @@ |
|||
extends ../layouts/html/system-message |
|||
block content |
|||
|
|||
address |
|||
div Action: Password reset request |
|||
div Account: #{recipient.username}@#{site.domain} |
|||
|
|||
p Someone has requested a password reset for your account on #{site.name}. If you did not make this request, please ignore this email and no further action will be taken. Your account information was not accessed by the person who made the request, and your password has not been changed. |
|||
|
|||
p If you did request a password reset due to a lost or stolen password, you can enter a new password here: |
|||
|
|||
.action-panel |
|||
a(href=`https://${site.domain}/auth/password-reset?t=${passwordResetToken.token}`).action-button Reset Password |
@ -0,0 +1,3 @@ |
|||
extends ../layouts/html/system-message |
|||
block message-body |
|||
.content-message!= htmlMessage |
@ -0,0 +1,4 @@ |
|||
extends ../layouts/html/system-message |
|||
block content |
|||
p Welcome to #{site.name}! Please visit #[a(href=`https://${site.domain}/email/verify?t=${emailVerifyToken}`)= `https://${site.domain}/email/verify?t=${emailVerifyToken}`] to verify your email address and enable all features on your new account. |
|||
p If you did not sign up for a new account at #{site.name}, please disregard this message. |
@ -0,0 +1,121 @@ |
|||
doctype html |
|||
html(lang='en') |
|||
head |
|||
meta(charset='UTF-8') |
|||
meta(name='viewport', content='width=device-width, initial-scale=1.0') |
|||
meta(name='description', content= pageDescription || siteDescription) |
|||
|
|||
title= pageTitle ? `${pageTitle} | ${site.name}` : site.name |
|||
|
|||
style(type="text/css"). |
|||
html, body { |
|||
padding: 0; |
|||
margin: 0; |
|||
background-color: #ffffff; |
|||
color: #1a1a1a; |
|||
} |
|||
|
|||
section { |
|||
padding: 20px 0; |
|||
background-color: #ffffff; |
|||
color: #1a1a1a; |
|||
} |
|||
|
|||
section.section-muted { |
|||
background-color: #f8f8f8; |
|||
color: #2a2a2a; |
|||
} |
|||
|
|||
.common-title { |
|||
max-width: 640px; |
|||
margin: 0 auto 8px auto; |
|||
font-size: 1.5em; |
|||
} |
|||
|
|||
.common-greeting { |
|||
max-width: 640px; |
|||
margin: 0 auto 20px auto; |
|||
font-size: 1.1em; |
|||
} |
|||
|
|||
.common-slogan { |
|||
max-width: 640px; |
|||
margin: 0 auto; |
|||
font-size: 1.1em; |
|||
font-style: italic; |
|||
} |
|||
|
|||
.content-message { |
|||
max-width: 640px; |
|||
margin: 0 auto; |
|||
background: white; |
|||
color: black; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.content-signature { |
|||
max-width: 640px; |
|||
margin: 0 auto; |
|||
background: white; |
|||
color: black; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.common-footer { |
|||
max-width: 640px; |
|||
margin: 0 auto; |
|||
font-size: 10px; |
|||
} |
|||
|
|||
.action-panel { |
|||
display: block; |
|||
box-sizing: border-box; |
|||
margin: 24px 0; |
|||
} |
|||
|
|||
.action-panel .action-button { |
|||
padding: 6px 20px; |
|||
margin: 0 24px; |
|||
|
|||
border: none; |
|||
border-radius: 20px; |
|||
outline: none; |
|||
|
|||
background-color: #1093de; |
|||
color: #ffffff; |
|||
|
|||
font-size: 16px; |
|||
font-weight: bold; |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.action-panel .action-button:hover { |
|||
text-decoration: underline; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.action-panel .action-button:first-child { |
|||
margin-left: 0; |
|||
} |
|||
|
|||
body |
|||
|
|||
include ../library |
|||
|
|||
section.section-muted |
|||
include ../../common/html/header |
|||
|
|||
section |
|||
.common-greeting |
|||
div Dear #{recipient.displayName || recipient.username}, |
|||
|
|||
block message-body |
|||
.content-message |
|||
block content |
|||
|
|||
.content-signature |
|||
p Thank you for your continued support! |
|||
p The #{site.name} team. |
|||
|
|||
section.section-muted |
|||
include ../../common/html/footer |
@ -0,0 +1,4 @@ |
|||
- |
|||
function formatCount (count) { |
|||
return numeral(count).format((count > 1000) ? '0,0.0a' : '0,0'); |
|||
} |
@ -0,0 +1,10 @@ |
|||
include ../library |
|||
include ../../common/text/header |
|||
| |
|||
block content |
|||
| |
|||
| Thank you for your continued support! |
|||
| |
|||
| The #{site.name} team. |
|||
| |
|||
include ../../common/text/footer |
@ -0,0 +1,5 @@ |
|||
extends ../layouts/text/system-message |
|||
block content |
|||
| |
|||
| #{textMessage} |
|||
| |
@ -0,0 +1,10 @@ |
|||
extends ../layouts/text/system-message |
|||
block content |
|||
| |
|||
| Action: Password reset request |
|||
| Account: #{recipient.username}@#{site.domain} |
|||
| |
|||
| Someone has requested a password reset for your account on #{site.name}. If you did not make this request, please ignore this email and no further action will be taken. Your account information was not accessed by the person who made the request, and your password has not been changed. |
|||
| |
|||
| If you did request a password reset due to a lost or stolen password, you can enter a new password here: #{`https://${site.domain}/auth/password-reset?t=${passwordResetToken.token}`}. |
|||
| |
@ -0,0 +1,5 @@ |
|||
extends ../layouts/text/system-message |
|||
block content |
|||
| |
|||
| #{textMessage} |
|||
| |
@ -0,0 +1,7 @@ |
|||
extends ../layouts/text/system-message |
|||
block content |
|||
| |
|||
| Welcome to #{site.name}! Please visit #{`https://${site.domain}/email/verify?t=${emailVerifyToken}`} to verify your email address and enable all features on your new account. |
|||
| |
|||
| If you did not sign up for a new account at #{site.name}, please disregard this message. |
|||
| |
@ -1,13 +1,9 @@ |
|||
include layout/main |
|||
block view-content |
|||
|
|||
section.uk-section.uk-section-default |
|||
section.uk-section.uk-section-default.uk-section-small |
|||
.uk-container |
|||
h1= site.name |
|||
div= site.description |
|||
|
|||
p The app is doing something else. |
|||
|
|||
p |
|||
span |
|||
i.fas.fa-home |
@ -0,0 +1,20 @@ |
|||
include ../layout/main |
|||
block view-content |
|||
|
|||
section.uk-section.uk-section-default.uk-section-small |
|||
.uk-container |
|||
h1.uk-margin-remove Welcome to #{site.name}! |
|||
|
|||
p #{site.name} is a real-time communications tool with high quality audio and video, extremely low latency, real-time text messaging, voicemail, and an easy-to-use interface built to remove everything unnecessary and focus on being good at making calls. |
|||
|
|||
div(uk-grid) |
|||
div(class="uk-width-1-1 uk-width-1-2@m") |
|||
.uk-margin-small |
|||
.uk-text-large New here? |
|||
div Start receiving and attending calls for free by creating a User Account. |
|||
a(href="/welcome/signup").uk-button.uk-button-primary.uk-button-large.uk-border-rounded Sign Up |
|||
div(class="uk-width-1-1 uk-width-1-2@m") |
|||
.uk-margin-small |
|||
.uk-text-large Returning member? |
|||
div Sign into your User Account with your username and password. |
|||
a(href="/welcome/login").uk-button.uk-button-secondary.uk-button-large.uk-border-rounded Login |
@ -0,0 +1,30 @@ |
|||
include ../layout/main |
|||
block view-content |
|||
|
|||
section.uk-section.uk-section-default.uk-section-small |
|||
.uk-container |
|||
h1= site.name |
|||
|
|||
.uk-card.uk-card-secondary.uk-card-small |
|||
.uk-card-header |
|||
h1.uk-card-title Create Account |
|||
.uk-card-body |
|||
form(method="POST", action="/welcome/signup").uk-form |
|||
.uk-margin |
|||
label(for="username").uk-form-label Username |
|||
input(id="username", name="username", type="text", maxlength="30", required, placeholder="Enter username").uk-input |
|||
div(uk-grid) |
|||
.uk-width-1-2 |
|||
.uk-margin |
|||
label(for="password").uk-form-label Password |
|||
input(id="password", name="password", type="password", required, placeholder="Enter password").uk-input |
|||
.uk-width-1-2 |
|||
.uk-margin |
|||
label(for="password-verify").uk-form-label Verify password |
|||
input(id="password-verify", name="passwordVerify", type="password", required, placeholder="Verify password").uk-input |
|||
.uk-card-footer |
|||
div(uk-grid).uk-flex-right |
|||
.uk-width-auto |
|||
a(href="/welcome").uk-button.uk-button-default Cancel |
|||
.uk-width-auto |
|||
button(type="submit").uk-button.uk-button-primary Create Account |
@ -0,0 +1,14 @@ |
|||
// job-queues.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
export default { |
|||
'reeeper': { |
|||
attempts: 3, |
|||
}, |
|||
'email': { |
|||
attempts: 3, |
|||
}, |
|||
}; |
@ -0,0 +1,218 @@ |
|||
// limiter.js
|
|||
// Copyright (C) 2024 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
const ONE_SECOND = 1000; |
|||
const ONE_MINUTE = ONE_SECOND * 60; |
|||
const ONE_HOUR = ONE_MINUTE * 60; |
|||
const ONE_DAY = ONE_HOUR * 24; |
|||
|
|||
export default { |
|||
|
|||
/* |
|||
* AuthController |
|||
*/ |
|||
auth: { |
|||
postOtpEnable: { |
|||
total: 5, |
|||
expire: ONE_MINUTE * 30, |
|||
message: 'You are enabling One-Time Passwords too quickly. Please try again later', |
|||
}, |
|||
postOtpAuthenticate: { |
|||
total: 5, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are trying One-Time Passwords too quickly. Please try again later', |
|||
}, |
|||
postForgotPassword: { |
|||
total: 5, |
|||
expire: ONE_DAY, |
|||
message: 'Password reset has been locked for one day. Please try again later.', |
|||
}, |
|||
postPasswordReset: { |
|||
total: 3, |
|||
expire: ONE_DAY, |
|||
message: 'Password reset has been locked for one day. Please try again later.', |
|||
}, |
|||
postLogin: { |
|||
total: 5, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are logging in too quickly', |
|||
}, |
|||
getPersonalApiToken: { |
|||
total: 20, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are requesting tokens too quickly', |
|||
}, |
|||
getSocketToken: { |
|||
total: 20, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are requesting tokens too quickly', |
|||
}, |
|||
getForgotPasswordForm: { |
|||
total: 3, |
|||
expire: ONE_HOUR, |
|||
message: 'Password reset has been locked for one day. Please try again later.', |
|||
}, |
|||
getResetPasswordForm: { |
|||
total: 5, |
|||
expire: ONE_DAY, |
|||
message: 'Password reset has been locked for one day. Please try again later.', |
|||
}, |
|||
getLogout: { |
|||
total: 10, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are logging out too quickly', |
|||
}, |
|||
}, |
|||
|
|||
/* |
|||
* EmailController |
|||
*/ |
|||
email: { |
|||
postEmailVerify: { |
|||
total: 10, |
|||
expire: ONE_HOUR, |
|||
message: "You are posting email verifications too quickly", |
|||
}, |
|||
getEmailOptOut: { |
|||
total: 10, |
|||
expire: ONE_HOUR, |
|||
message: "You are opting out too quickly", |
|||
}, |
|||
getEmailVerify: { |
|||
total: 10, |
|||
expire: ONE_HOUR, |
|||
message: "You are requesting the email verification form too quickly", |
|||
}, |
|||
}, |
|||
|
|||
/* |
|||
* HomeController |
|||
*/ |
|||
home: { |
|||
getHome: { |
|||
total: 20, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are loading the home page too quickly', |
|||
} |
|||
}, |
|||
|
|||
/* |
|||
* ImageController |
|||
*/ |
|||
image: { |
|||
postCreateImage: { |
|||
total: 5, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are uploading images too quickly', |
|||
}, |
|||
getProxyImage: { |
|||
total: 500, |
|||
expire: ONE_SECOND * 10, |
|||
message: 'You are requesting proxy images too quickly', |
|||
}, |
|||
getImage: { |
|||
total: 500, |
|||
expire: ONE_SECOND * 10, |
|||
message: 'You are requesting images too quickly', |
|||
}, |
|||
deleteImage: { |
|||
total: 60, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are deleting images too quickly', |
|||
}, |
|||
}, |
|||
|
|||
/* |
|||
* ManifestController |
|||
*/ |
|||
manifest: { |
|||
getManifest: { |
|||
total: 60, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are fetching application manifests too quickly', |
|||
} |
|||
}, |
|||
|
|||
/* |
|||
* UserController |
|||
*/ |
|||
user: { |
|||
postCreateUser: { |
|||
total: 4, |
|||
expire: ONE_HOUR * 4, |
|||
message: 'You are creating accounts too quickly', |
|||
}, |
|||
postProfilePhoto: { |
|||
total: 5, |
|||
expire: ONE_MINUTE * 5, |
|||
message: 'You are updating your profile photo too quickly', |
|||
}, |
|||
postUpdateSettings: { |
|||
total: 6, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are updating account settings too quickly', |
|||
}, |
|||
postBlockUser: { |
|||
total: 10, |
|||
expire: ONE_HOUR, |
|||
message: 'You are blocking people too quickly', |
|||
}, |
|||
getResendWelcomeEmail: { |
|||
total: 3, |
|||
expire: ONE_HOUR, |
|||
message: 'You are sending welcome emails too often', |
|||
}, |
|||
getOtpSetup: { |
|||
total: 10, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are configuring two-factor authentication too quickly', |
|||
}, |
|||
getOtpDisable: { |
|||
total: 10, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are disabling two-factor authentication too quickly', |
|||
}, |
|||
getStripeCustomerPortal: { |
|||
total: 4, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are accessing the Stripe Customer Portal too quickly', |
|||
}, |
|||
getSettings: { |
|||
total: 8, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are requesting user settings too quickly', |
|||
}, |
|||
getBlockView: { |
|||
total: 10, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are loading your block list too quickly', |
|||
}, |
|||
getUserProfile: { |
|||
total: 12, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are requesting user profiles too quickly', |
|||
}, |
|||
deleteProfilePhoto: { |
|||
total: 5, |
|||
expire: ONE_MINUTE * 5, |
|||
message: 'You are deleting your profile photo too quickly', |
|||
}, |
|||
deleteBlockedUser: { |
|||
total: 30, |
|||
expire: ONE_HOUR, |
|||
message: 'You are un-blocking people too quickly', |
|||
}, |
|||
}, |
|||
|
|||
/* |
|||
* WelcomeController |
|||
*/ |
|||
welcome: { |
|||
total: 12, |
|||
expire: ONE_MINUTE, |
|||
message: 'You are loading these pages too quickly', |
|||
}, |
|||
}; |
@ -0,0 +1,22 @@ |
|||
// reserved-names.js
|
|||
// Copyright (C) 2022,2023 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
export default [ |
|||
'admin', |
|||
'announcement', |
|||
'auth', |
|||
'category', |
|||
'comment', |
|||
'community', |
|||
'dashboard', |
|||
'email', |
|||
'image', |
|||
'manifest', |
|||
'user', |
|||
'welcome', |
|||
'digitaltelepresence', |
|||
'dtp', |
|||
]; |
@ -9,7 +9,7 @@ |
|||
}, |
|||
"main": "dtp-chat.js", |
|||
"scripts": { |
|||
"develop": "nodemon dtp-chat.js", |
|||
"dev": "nodemon dtp-chat.js", |
|||
"build": "NODE_ENV=production yarn webpack --config webpack.config.js" |
|||
}, |
|||
"repository": "[email protected]:digital-telepresence/dtp-chat.git", |
|||
@ -21,9 +21,19 @@ |
|||
"@socket.io/redis-adapter": "^8.3.0", |
|||
"@socket.io/redis-emitter": "^5.1.0", |
|||
"ansicolor": "^2.0.3", |
|||
"bull": "^4.12.2", |
|||
"chart.js": "^4.4.2", |
|||
"connect-redis": "^7.1.1", |
|||
"cookie-parser": "^1.4.6", |
|||
"diacritics": "^1.3.0", |
|||
"disposable-email-provider-domains": "^1.0.9", |
|||
"dotenv": "^16.4.5", |
|||
"email-domain-check": "^1.1.4", |
|||
"email-validator": "^2.0.4", |
|||
"express": "^4.19.2", |
|||
"glob": "^10.3.10", |
|||
"express-limiter": "^1.6.1", |
|||
"express-session": "^1.18.0", |
|||
"highlight.js": "^11.9.0", |
|||
"ioredis": "^5.3.2", |
|||
"marked": "^12.0.1", |
|||
"mediasoup": "^3.13.24", |
|||
@ -31,13 +41,19 @@ |
|||
"mongoose": "^8.2.4", |
|||
"morgan": "^1.10.0", |
|||
"multer": "^1.4.5-lts.1", |
|||
"nodemailer": "^6.9.13", |
|||
"numeral": "^2.0.6", |
|||
"passport": "^0.7.0", |
|||
"passport-local": "^1.0.0", |
|||
"pretty-checkbox": "^3.0.3", |
|||
"pug": "^3.0.2", |
|||
"rotating-file-stream": "^3.2.1", |
|||
"shoetest": "^1.2.2", |
|||
"slug": "^9.0.0", |
|||
"socket.io": "^4.7.5", |
|||
"striptags": "^3.2.0" |
|||
"striptags": "^3.2.0", |
|||
"unzalgo": "^3.0.0", |
|||
"uuid": "^9.0.1" |
|||
}, |
|||
"devDependencies": { |
|||
"browser-sync": "^3.0.2", |
|||
|
@ -966,18 +966,6 @@ |
|||
resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" |
|||
integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== |
|||
|
|||
"@isaacs/cliui@^8.0.2": |
|||
version "8.0.2" |
|||
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" |
|||
integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== |
|||
dependencies: |
|||
string-width "^5.1.2" |
|||
string-width-cjs "npm:string-width@^4.2.0" |
|||
strip-ansi "^7.0.1" |
|||
strip-ansi-cjs "npm:strip-ansi@^6.0.1" |
|||
wrap-ansi "^8.1.0" |
|||
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" |
|||
|
|||
"@jridgewell/gen-mapping@^0.3.5": |
|||
version "0.3.5" |
|||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" |
|||
@ -1018,6 +1006,11 @@ |
|||
"@jridgewell/resolve-uri" "^3.1.0" |
|||
"@jridgewell/sourcemap-codec" "^1.4.14" |
|||
|
|||
"@kurkle/color@^0.3.0": |
|||
version "0.3.2" |
|||
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" |
|||
integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== |
|||
|
|||
"@mongodb-js/saslprep@^1.1.0": |
|||
version "1.1.5" |
|||
resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.5.tgz#0c48a96c8d799e81fae311b7251aa5c1dc7c6e95" |
|||
@ -1025,10 +1018,35 @@ |
|||
dependencies: |
|||
sparse-bitfield "^3.0.3" |
|||
|
|||
"@pkgjs/parseargs@^0.11.0": |
|||
version "0.11.0" |
|||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" |
|||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== |
|||
"@msgpackr-extract/[email protected]": |
|||
version "3.0.2" |
|||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38" |
|||
integrity sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ== |
|||
|
|||
"@msgpackr-extract/[email protected]": |
|||
version "3.0.2" |
|||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz#f954f34355712212a8e06c465bc06c40852c6bb3" |
|||
integrity sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw== |
|||
|
|||
"@msgpackr-extract/[email protected]": |
|||
version "3.0.2" |
|||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz#45c63037f045c2b15c44f80f0393fa24f9655367" |
|||
integrity sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg== |
|||
|
|||
"@msgpackr-extract/[email protected]": |
|||
version "3.0.2" |
|||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz#35707efeafe6d22b3f373caf9e8775e8920d1399" |
|||
integrity sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA== |
|||
|
|||
"@msgpackr-extract/[email protected]": |
|||
version "3.0.2" |
|||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz#091b1218b66c341f532611477ef89e83f25fae4f" |
|||
integrity sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA== |
|||
|
|||
"@msgpackr-extract/[email protected]": |
|||
version "3.0.2" |
|||
resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz#0f164b726869f71da3c594171df5ebc1c4b0a407" |
|||
integrity sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ== |
|||
|
|||
"@rollup/plugin-babel@^5.2.0": |
|||
version "5.3.1" |
|||
@ -1430,11 +1448,6 @@ ansi-regex@^5.0.1: |
|||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" |
|||
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== |
|||
|
|||
ansi-regex@^6.0.1: |
|||
version "6.0.1" |
|||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" |
|||
integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== |
|||
|
|||
ansi-styles@^3.2.1: |
|||
version "3.2.1" |
|||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" |
|||
@ -1449,11 +1462,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: |
|||
dependencies: |
|||
color-convert "^2.0.1" |
|||
|
|||
ansi-styles@^6.1.0: |
|||
version "6.2.1" |
|||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" |
|||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== |
|||
|
|||
[email protected], ansi-wrap@^0.1.0: |
|||
version "0.1.0" |
|||
resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" |
|||
@ -1514,7 +1522,7 @@ arraybuffer.prototype.slice@^1.0.3: |
|||
is-array-buffer "^3.0.4" |
|||
is-shared-array-buffer "^1.0.2" |
|||
|
|||
asap@~2.0.3: |
|||
asap@^2.0.6, asap@~2.0.3: |
|||
version "2.0.6" |
|||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" |
|||
integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== |
|||
@ -1749,6 +1757,19 @@ builtin-modules@^3.1.0: |
|||
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" |
|||
integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== |
|||
|
|||
bull@^4.12.2: |
|||
version "4.12.2" |
|||
resolved "https://registry.yarnpkg.com/bull/-/bull-4.12.2.tgz#302ee8f35fd37c31baf58817cce9a2c6930c3c5d" |
|||
integrity sha512-WPuc0VCYx+cIVMiZtPwRpWyyJFBrj4/OgKJ6n9Jf4tIw7rQNV+HAKQv15UDkcTvfpGFehvod7Fd1YztbYSJIDQ== |
|||
dependencies: |
|||
cron-parser "^4.2.1" |
|||
get-port "^5.1.1" |
|||
ioredis "^5.3.2" |
|||
lodash "^4.17.21" |
|||
msgpackr "^1.10.1" |
|||
semver "^7.5.2" |
|||
uuid "^8.3.0" |
|||
|
|||
busboy@^1.0.0: |
|||
version "1.6.0" |
|||
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" |
|||
@ -1801,6 +1822,13 @@ character-parser@^2.2.0: |
|||
dependencies: |
|||
is-regex "^1.0.3" |
|||
|
|||
chart.js@^4.4.2: |
|||
version "4.4.2" |
|||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.2.tgz#95962fa6430828ed325a480cc2d5f2b4e385ac31" |
|||
integrity sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg== |
|||
dependencies: |
|||
"@kurkle/color" "^0.3.0" |
|||
|
|||
chokidar@^3.5.1, chokidar@^3.5.2: |
|||
version "3.6.0" |
|||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" |
|||
@ -1881,6 +1909,11 @@ cluster-key-slot@^1.1.0: |
|||
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" |
|||
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== |
|||
|
|||
co@^4.6.0: |
|||
version "4.6.0" |
|||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" |
|||
integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== |
|||
|
|||
color-convert@^1.9.0: |
|||
version "1.9.3" |
|||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" |
|||
@ -1950,6 +1983,11 @@ connect-history-api-fallback@^1: |
|||
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" |
|||
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== |
|||
|
|||
connect-redis@^7.1.1: |
|||
version "7.1.1" |
|||
resolved "https://registry.yarnpkg.com/connect-redis/-/connect-redis-7.1.1.tgz#b78f91eb6d7509ae9e819bb362b94ba459072a1d" |
|||
integrity sha512-M+z7alnCJiuzKa8/1qAYdGUXHYfDnLolOGAUjOioB07pP39qxjG+X9ibsud7qUBc4jMV5Mcy3ugGv8eFcgamJQ== |
|||
|
|||
[email protected]: |
|||
version "3.6.6" |
|||
resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.6.tgz#09eff6c55af7236e137135a72574858b6786f524" |
|||
@ -1992,11 +2030,29 @@ convert-source-map@^2.0.0: |
|||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" |
|||
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== |
|||
|
|||
cookie-parser@^1.4.6: |
|||
version "1.4.6" |
|||
resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" |
|||
integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== |
|||
dependencies: |
|||
cookie "0.4.1" |
|||
cookie-signature "1.0.6" |
|||
|
|||
[email protected]: |
|||
version "1.0.6" |
|||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" |
|||
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== |
|||
|
|||
[email protected]: |
|||
version "1.0.7" |
|||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454" |
|||
integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA== |
|||
|
|||
[email protected]: |
|||
version "0.4.1" |
|||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" |
|||
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== |
|||
|
|||
[email protected]: |
|||
version "0.6.0" |
|||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" |
|||
@ -2034,7 +2090,14 @@ cors@~2.8.5: |
|||
object-assign "^4" |
|||
vary "^1" |
|||
|
|||
cross-spawn@^7.0.0, cross-spawn@^7.0.3: |
|||
cron-parser@^4.2.1: |
|||
version "4.9.0" |
|||
resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5" |
|||
integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q== |
|||
dependencies: |
|||
luxon "^3.2.1" |
|||
|
|||
cross-spawn@^7.0.3: |
|||
version "7.0.3" |
|||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" |
|||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== |
|||
@ -2178,6 +2241,24 @@ dev-ip@^1.0.1: |
|||
resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0" |
|||
integrity sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A== |
|||
|
|||
diacritics@^1.3.0: |
|||
version "1.3.0" |
|||
resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" |
|||
integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA== |
|||
|
|||
disposable-email-provider-domains@^1.0.9: |
|||
version "1.0.9" |
|||
resolved "https://registry.yarnpkg.com/disposable-email-provider-domains/-/disposable-email-provider-domains-1.0.9.tgz#0ac18ca5477a8d5e6f7f53c5862de8f0dcdee055" |
|||
integrity sha512-6/8yqrRlRSZvpfKoR11Sk+S3Vv65jR3AN7Q6F81xusnPRNQ4cna+dOv5ZdTYUOJky6dKY/43vTNK7V5M7q4f7Q== |
|||
|
|||
dnscache@^1.0.1: |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/dnscache/-/dnscache-1.0.2.tgz#fd3c24d66c141625f594c77be7a8dafee2a66c8a" |
|||
integrity sha512-2FFKzmLGOnD+Y378bRKH+gTjRMuSpH7OKgPy31KjjfCoKZx7tU8Dmqfd/3fhG2d/4bppuN8/KtWMUZBAcUCRnQ== |
|||
dependencies: |
|||
asap "^2.0.6" |
|||
lodash.clone "^4.5.0" |
|||
|
|||
doctypes@^1.1.0: |
|||
version "1.1.0" |
|||
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" |
|||
@ -2221,10 +2302,10 @@ dotenv@^16.4.5: |
|||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" |
|||
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== |
|||
|
|||
eastasianwidth@^0.2.0: |
|||
version "0.2.0" |
|||
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" |
|||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== |
|||
drange@^1.0.2: |
|||
version "1.1.1" |
|||
resolved "https://registry.yarnpkg.com/drange/-/drange-1.1.1.tgz#b2aecec2aab82fcef11dbbd7b9e32b83f8f6c0b8" |
|||
integrity sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA== |
|||
|
|||
easy-extender@^2.3.4: |
|||
version "2.3.4" |
|||
@ -2257,16 +2338,24 @@ electron-to-chromium@^1.4.668: |
|||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.716.tgz#90c229ce0af2ad3b6e54472af1200e07f10293a4" |
|||
integrity sha512-t/MXMzFKQC3UfMDpw7V5wdB/UAB8dWx4hEsy+fpPYJWW3gqh3u5T1uXp6vR+H6dGCPBxkRo+YBcapBLvbGQHRw== |
|||
|
|||
email-domain-check@^1.1.4: |
|||
version "1.1.4" |
|||
resolved "https://registry.yarnpkg.com/email-domain-check/-/email-domain-check-1.1.4.tgz#b4001caaf7f7fa15fb8604dbb0739d2179578e8a" |
|||
integrity sha512-Yz+hjRaJqogl1j58UAJgrgxPnFd8R4kRW5JS145C4QZHVsSzJTSSSXW61GoqI9RbSRhDRB0vBS33fUyBki7AOw== |
|||
dependencies: |
|||
co "^4.6.0" |
|||
dnscache "^1.0.1" |
|||
|
|||
email-validator@^2.0.4: |
|||
version "2.0.4" |
|||
resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed" |
|||
integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ== |
|||
|
|||
emoji-regex@^8.0.0: |
|||
version "8.0.0" |
|||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" |
|||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== |
|||
|
|||
emoji-regex@^9.2.2: |
|||
version "9.2.2" |
|||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" |
|||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== |
|||
|
|||
encodeurl@~1.0.1, encodeurl@~1.0.2: |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" |
|||
@ -2498,6 +2587,25 @@ [email protected], [email protected]: |
|||
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" |
|||
integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== |
|||
|
|||
express-limiter@^1.6.1: |
|||
version "1.6.1" |
|||
resolved "https://registry.yarnpkg.com/express-limiter/-/express-limiter-1.6.1.tgz#70ede144f9e875e3c7e120644018a6f7784bda71" |
|||
integrity sha512-w/Xz/FIHuAOIVIUeHSe6g2rSYTqCSKA9WFLO2CxX15BzEAK+avp7HoYd7pu/M2tEp5E/to253+4x8vQ6WcTJkQ== |
|||
|
|||
express-session@^1.18.0: |
|||
version "1.18.0" |
|||
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.0.tgz#a6ae39d9091f2efba5f20fc5c65a3ce7c9ce16a3" |
|||
integrity sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ== |
|||
dependencies: |
|||
cookie "0.6.0" |
|||
cookie-signature "1.0.7" |
|||
debug "2.6.9" |
|||
depd "~2.0.0" |
|||
on-headers "~1.0.2" |
|||
parseurl "~1.3.3" |
|||
safe-buffer "5.2.1" |
|||
uid-safe "~2.1.5" |
|||
|
|||
express@^4.19.2: |
|||
version "4.19.2" |
|||
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" |
|||
@ -2646,14 +2754,6 @@ for-each@^0.3.3: |
|||
dependencies: |
|||
is-callable "^1.1.3" |
|||
|
|||
foreground-child@^3.1.0: |
|||
version "3.1.1" |
|||
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" |
|||
integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== |
|||
dependencies: |
|||
cross-spawn "^7.0.0" |
|||
signal-exit "^4.0.1" |
|||
|
|||
formdata-polyfill@^4.0.10: |
|||
version "4.0.10" |
|||
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" |
|||
@ -2753,6 +2853,11 @@ get-own-enumerable-property-symbols@^3.0.0: |
|||
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" |
|||
integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== |
|||
|
|||
get-port@^5.1.1: |
|||
version "5.1.1" |
|||
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" |
|||
integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== |
|||
|
|||
get-symbol-description@^1.0.2: |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" |
|||
@ -2774,17 +2879,6 @@ glob-to-regexp@^0.4.1: |
|||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" |
|||
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== |
|||
|
|||
glob@^10.3.10: |
|||
version "10.3.10" |
|||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" |
|||
integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== |
|||
dependencies: |
|||
foreground-child "^3.1.0" |
|||
jackspeak "^2.3.5" |
|||
minimatch "^9.0.1" |
|||
minipass "^5.0.0 || ^6.0.2 || ^7.0.0" |
|||
path-scurry "^1.10.1" |
|||
|
|||
glob@^7.1.1, glob@^7.1.6: |
|||
version "7.2.3" |
|||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" |
|||
@ -2875,6 +2969,11 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: |
|||
dependencies: |
|||
function-bind "^1.1.2" |
|||
|
|||
highlight.js@^11.9.0: |
|||
version "11.9.0" |
|||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0" |
|||
integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw== |
|||
|
|||
[email protected]: |
|||
version "3.8.3" |
|||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" |
|||
@ -3237,20 +3336,16 @@ isexe@^2.0.0: |
|||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" |
|||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== |
|||
|
|||
isnumber@~1.0.0: |
|||
version "1.0.0" |
|||
resolved "https://registry.yarnpkg.com/isnumber/-/isnumber-1.0.0.tgz#0e3f9759b581d99dd85086f0ec2a74909cfadd01" |
|||
integrity sha512-JLiSz/zsZcGFXPrB4I/AGBvtStkt+8QmksyZBZnVXnnK9XdTEyz0tX8CRYljtwYDuIuZzih6DpHQdi+3Q6zHPw== |
|||
|
|||
isobject@^3.0.1: |
|||
version "3.0.1" |
|||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" |
|||
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== |
|||
|
|||
jackspeak@^2.3.5: |
|||
version "2.3.6" |
|||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" |
|||
integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== |
|||
dependencies: |
|||
"@isaacs/cliui" "^8.0.2" |
|||
optionalDependencies: |
|||
"@pkgjs/parseargs" "^0.11.0" |
|||
|
|||
jake@^10.8.5: |
|||
version "10.8.7" |
|||
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f" |
|||
@ -3420,7 +3515,7 @@ locate-path@^5.0.0: |
|||
dependencies: |
|||
p-locate "^4.1.0" |
|||
|
|||
lodash.clone@^4.3.2: |
|||
lodash.clone@^4.3.2, lodash.clone@^4.5.0: |
|||
version "4.5.0" |
|||
resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" |
|||
integrity sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg== |
|||
@ -3455,16 +3550,11 @@ lodash.sortby@^4.7.0: |
|||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" |
|||
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== |
|||
|
|||
lodash@^4, lodash@^4.17.10, lodash@^4.17.14, lodash@^4.17.20, lodash@~4.17.21: |
|||
lodash@^4, lodash@^4.17.10, lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17.21: |
|||
version "4.17.21" |
|||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" |
|||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== |
|||
|
|||
lru-cache@^10.2.0: |
|||
version "10.2.0" |
|||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" |
|||
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== |
|||
|
|||
lru-cache@^5.1.1: |
|||
version "5.1.1" |
|||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" |
|||
@ -3479,6 +3569,11 @@ lru-cache@^6.0.0: |
|||
dependencies: |
|||
yallist "^4.0.0" |
|||
|
|||
luxon@^3.2.1: |
|||
version "3.4.4" |
|||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af" |
|||
integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== |
|||
|
|||
magic-string@^0.25.0, magic-string@^0.25.7: |
|||
version "0.25.9" |
|||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" |
|||
@ -3605,13 +3700,6 @@ minimatch@^5.0.1: |
|||
dependencies: |
|||
brace-expansion "^2.0.1" |
|||
|
|||
minimatch@^9.0.1: |
|||
version "9.0.3" |
|||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" |
|||
integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== |
|||
dependencies: |
|||
brace-expansion "^2.0.1" |
|||
|
|||
minimatch@~3.0.2: |
|||
version "3.0.8" |
|||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" |
|||
@ -3636,11 +3724,6 @@ minipass@^5.0.0: |
|||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" |
|||
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== |
|||
|
|||
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": |
|||
version "7.0.4" |
|||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" |
|||
integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== |
|||
|
|||
minizlib@^2.1.1: |
|||
version "2.1.2" |
|||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" |
|||
@ -3739,6 +3822,27 @@ [email protected], ms@^2.1.1: |
|||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" |
|||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== |
|||
|
|||
msgpackr-extract@^3.0.2: |
|||
version "3.0.2" |
|||
resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz#e05ec1bb4453ddf020551bcd5daaf0092a2c279d" |
|||
integrity sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A== |
|||
dependencies: |
|||
node-gyp-build-optional-packages "5.0.7" |
|||
optionalDependencies: |
|||
"@msgpackr-extract/msgpackr-extract-darwin-arm64" "3.0.2" |
|||
"@msgpackr-extract/msgpackr-extract-darwin-x64" "3.0.2" |
|||
"@msgpackr-extract/msgpackr-extract-linux-arm" "3.0.2" |
|||
"@msgpackr-extract/msgpackr-extract-linux-arm64" "3.0.2" |
|||
"@msgpackr-extract/msgpackr-extract-linux-x64" "3.0.2" |
|||
"@msgpackr-extract/msgpackr-extract-win32-x64" "3.0.2" |
|||
|
|||
msgpackr@^1.10.1: |
|||
version "1.10.1" |
|||
resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.10.1.tgz#51953bb4ce4f3494f0c4af3f484f01cfbb306555" |
|||
integrity sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ== |
|||
optionalDependencies: |
|||
msgpackr-extract "^3.0.2" |
|||
|
|||
multer@^1.4.5-lts.1: |
|||
version "1.4.5-lts.1" |
|||
resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.5-lts.1.tgz#803e24ad1984f58edffbc79f56e305aec5cfd1ac" |
|||
@ -3789,11 +3893,21 @@ node-fetch@^3.3.2: |
|||
fetch-blob "^3.1.4" |
|||
formdata-polyfill "^4.0.10" |
|||
|
|||
[email protected]: |
|||
version "5.0.7" |
|||
resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3" |
|||
integrity sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w== |
|||
|
|||
node-releases@^2.0.14: |
|||
version "2.0.14" |
|||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" |
|||
integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== |
|||
|
|||
nodemailer@^6.9.13: |
|||
version "6.9.13" |
|||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.13.tgz#5b292bf1e92645f4852ca872c56a6ba6c4a3d3d6" |
|||
integrity sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA== |
|||
|
|||
nodemon-webpack-plugin@^4.8.2: |
|||
version "4.8.2" |
|||
resolved "https://registry.yarnpkg.com/nodemon-webpack-plugin/-/nodemon-webpack-plugin-4.8.2.tgz#c42e15f754a8a5645174885ad7b04396a549db16" |
|||
@ -3927,6 +4041,27 @@ parseurl@~1.3.2, parseurl@~1.3.3: |
|||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" |
|||
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== |
|||
|
|||
passport-local@^1.0.0: |
|||
version "1.0.0" |
|||
resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" |
|||
integrity sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow== |
|||
dependencies: |
|||
passport-strategy "1.x.x" |
|||
|
|||
[email protected]: |
|||
version "1.0.0" |
|||
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" |
|||
integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== |
|||
|
|||
passport@^0.7.0: |
|||
version "0.7.0" |
|||
resolved "https://registry.yarnpkg.com/passport/-/passport-0.7.0.tgz#3688415a59a48cf8068417a8a8092d4492ca3a05" |
|||
integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== |
|||
dependencies: |
|||
passport-strategy "1.x.x" |
|||
pause "0.0.1" |
|||
utils-merge "^1.0.1" |
|||
|
|||
path-exists@^4.0.0: |
|||
version "4.0.0" |
|||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" |
|||
@ -3947,19 +4082,16 @@ path-parse@^1.0.7: |
|||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" |
|||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== |
|||
|
|||
path-scurry@^1.10.1: |
|||
version "1.10.2" |
|||
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7" |
|||
integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA== |
|||
dependencies: |
|||
lru-cache "^10.2.0" |
|||
minipass "^5.0.0 || ^6.0.2 || ^7.0.0" |
|||
|
|||
[email protected]: |
|||
version "0.1.7" |
|||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" |
|||
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== |
|||
|
|||
[email protected]: |
|||
version "0.0.1" |
|||
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" |
|||
integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== |
|||
|
|||
picocolors@^1.0.0: |
|||
version "1.0.0" |
|||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" |
|||
@ -4210,6 +4342,19 @@ [email protected]: |
|||
dependencies: |
|||
side-channel "^1.0.4" |
|||
|
|||
randexp@^0.5.3: |
|||
version "0.5.3" |
|||
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.5.3.tgz#f31c2de3148b30bdeb84b7c3f59b0ebb9fec3738" |
|||
integrity sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w== |
|||
dependencies: |
|||
drange "^1.0.2" |
|||
ret "^0.2.0" |
|||
|
|||
random-bytes@~1.0.0: |
|||
version "1.0.0" |
|||
resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" |
|||
integrity sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ== |
|||
|
|||
randombytes@^2.1.0: |
|||
version "2.1.0" |
|||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" |
|||
@ -4388,6 +4533,11 @@ [email protected]: |
|||
debug "^2.2.0" |
|||
minimatch "^3.0.2" |
|||
|
|||
ret@^0.2.0: |
|||
version "0.2.2" |
|||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" |
|||
integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ== |
|||
|
|||
rollup-plugin-terser@^7.0.0: |
|||
version "7.0.2" |
|||
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" |
|||
@ -4483,7 +4633,7 @@ semver@^6.3.1: |
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" |
|||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== |
|||
|
|||
semver@^7.5.3, semver@^7.5.4: |
|||
semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: |
|||
version "7.6.0" |
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" |
|||
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== |
|||
@ -4631,6 +4781,13 @@ shebang-regex@^3.0.0: |
|||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" |
|||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== |
|||
|
|||
shoetest@^1.2.2: |
|||
version "1.2.2" |
|||
resolved "https://registry.yarnpkg.com/shoetest/-/shoetest-1.2.2.tgz#1029dacd8124d80162a9ba24a454ed8d5af95718" |
|||
integrity sha512-iT8kIEFcGfUwo53VUFckm+glTkc0oLycRe+YqU/W4wQuIHGIWc5KMIpDnJVdavKCyEZKQTi8IDq27rDmB09QjA== |
|||
dependencies: |
|||
randexp "^0.5.3" |
|||
|
|||
side-channel@^1.0.4, side-channel@^1.0.6: |
|||
version "1.0.6" |
|||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" |
|||
@ -4646,11 +4803,6 @@ [email protected]: |
|||
resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053" |
|||
integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ== |
|||
|
|||
signal-exit@^4.0.1: |
|||
version "4.1.0" |
|||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" |
|||
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== |
|||
|
|||
simple-update-notifier@^2.0.0: |
|||
version "2.0.0" |
|||
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" |
|||
@ -4749,6 +4901,13 @@ standard-as-callback@^2.1.0: |
|||
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" |
|||
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== |
|||
|
|||
stats-lite@^2.2.0: |
|||
version "2.2.0" |
|||
resolved "https://registry.yarnpkg.com/stats-lite/-/stats-lite-2.2.0.tgz#278a5571fa1d2e8b1691295dccc0235282393bbf" |
|||
integrity sha512-/Kz55rgUIv2KP2MKphwYT/NCuSfAlbbMRv2ZWw7wyXayu230zdtzhxxuXXcvsc6EmmhS8bSJl3uS1wmMHFumbA== |
|||
dependencies: |
|||
isnumber "~1.0.0" |
|||
|
|||
[email protected]: |
|||
version "2.0.1" |
|||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" |
|||
@ -4782,7 +4941,7 @@ streamsearch@^1.1.0: |
|||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" |
|||
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== |
|||
|
|||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: |
|||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: |
|||
name string-width-cjs |
|||
version "4.2.3" |
|||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" |
|||
@ -4792,15 +4951,6 @@ streamsearch@^1.1.0: |
|||
is-fullwidth-code-point "^3.0.0" |
|||
strip-ansi "^6.0.1" |
|||
|
|||
string-width@^5.0.1, string-width@^5.1.2: |
|||
version "5.1.2" |
|||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" |
|||
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== |
|||
dependencies: |
|||
eastasianwidth "^0.2.0" |
|||
emoji-regex "^9.2.2" |
|||
strip-ansi "^7.0.1" |
|||
|
|||
string.prototype.matchall@^4.0.6: |
|||
version "4.0.11" |
|||
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" |
|||
@ -4868,7 +5018,7 @@ stringify-object@^3.3.0: |
|||
is-obj "^1.0.1" |
|||
is-regexp "^1.0.0" |
|||
|
|||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: |
|||
strip-ansi@^6.0.0, strip-ansi@^6.0.1: |
|||
name strip-ansi-cjs |
|||
version "6.0.1" |
|||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" |
|||
@ -4876,13 +5026,6 @@ stringify-object@^3.3.0: |
|||
dependencies: |
|||
ansi-regex "^5.0.1" |
|||
|
|||
strip-ansi@^7.0.1: |
|||
version "7.1.0" |
|||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" |
|||
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== |
|||
dependencies: |
|||
ansi-regex "^6.0.1" |
|||
|
|||
strip-comments@^2.0.1: |
|||
version "2.0.1" |
|||
resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" |
|||
@ -5112,6 +5255,13 @@ ua-parser-js@^1.0.33: |
|||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f" |
|||
integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ== |
|||
|
|||
uid-safe@~2.1.5: |
|||
version "2.1.5" |
|||
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" |
|||
integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== |
|||
dependencies: |
|||
random-bytes "~1.0.0" |
|||
|
|||
[email protected]: |
|||
version "1.0.0" |
|||
resolved "https://registry.yarnpkg.com/uid2/-/uid2-1.0.0.tgz#ef8d95a128d7c5c44defa1a3d052eecc17a06bfb" |
|||
@ -5187,6 +5337,13 @@ [email protected], unpipe@~1.0.0: |
|||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" |
|||
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== |
|||
|
|||
unzalgo@^3.0.0: |
|||
version "3.0.0" |
|||
resolved "https://registry.yarnpkg.com/unzalgo/-/unzalgo-3.0.0.tgz#e9c9dbbf3dfa52c11075d93d57b92c50421bcf99" |
|||
integrity sha512-yRSDlFaYpFJK2VO0iI4I2E3l1CF8puFNL00nh7beZ/q4XSxd9XPNIlsTvfOz/fF2P6tMBLWNVLWpLBvJ9/11ZQ== |
|||
dependencies: |
|||
stats-lite "^2.2.0" |
|||
|
|||
upath@^1.2.0: |
|||
version "1.2.0" |
|||
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" |
|||
@ -5212,11 +5369,21 @@ util-deprecate@^1.0.2, util-deprecate@~1.0.1: |
|||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" |
|||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== |
|||
|
|||
[email protected]: |
|||
[email protected], utils-merge@^1.0.1: |
|||
version "1.0.1" |
|||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" |
|||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== |
|||
|
|||
uuid@^8.3.0: |
|||
version "8.3.2" |
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" |
|||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== |
|||
|
|||
uuid@^9.0.1: |
|||
version "9.0.1" |
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" |
|||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== |
|||
|
|||
vary@^1, vary@~1.1.2: |
|||
version "1.1.2" |
|||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" |
|||
@ -5589,7 +5756,7 @@ [email protected]: |
|||
"@types/trusted-types" "^2.0.2" |
|||
workbox-core "7.0.0" |
|||
|
|||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: |
|||
wrap-ansi@^7.0.0: |
|||
name wrap-ansi-cjs |
|||
version "7.0.0" |
|||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" |
|||
@ -5599,15 +5766,6 @@ [email protected]: |
|||
string-width "^4.1.0" |
|||
strip-ansi "^6.0.0" |
|||
|
|||
wrap-ansi@^8.1.0: |
|||
version "8.1.0" |
|||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" |
|||
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== |
|||
dependencies: |
|||
ansi-styles "^6.1.0" |
|||
string-width "^5.0.1" |
|||
strip-ansi "^7.0.1" |
|||
|
|||
wrappy@1: |
|||
version "1.0.2" |
|||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" |
|||
|
Loading…
Reference in new issue