Browse Source

migrating old DTP to new DTP

develop
Rob Colbert 1 year ago
parent
commit
02f1b7c029
  1. 2
      README.md
  2. 13
      app/controllers/home.js
  3. 73
      app/controllers/manifest.js
  4. 71
      app/controllers/welcome.js
  5. 18
      app/models/auth-token.js
  6. 33
      app/models/email-blacklist.js
  7. 19
      app/models/email-log.js
  8. 17
      app/models/email-verify.js
  9. 20
      app/models/email.js
  10. 13
      app/models/user.js
  11. 84
      app/services/auth-token.js
  12. 84
      app/services/cache.js
  13. 61
      app/services/crypto.js
  14. 315
      app/services/email.js
  15. 64
      app/services/job-queue.js
  16. 77
      app/services/lib/edit-with-vi.js
  17. 64
      app/services/limiter.js
  18. 98
      app/services/session.js
  19. 71
      app/services/text.js
  20. 664
      app/services/user.js
  21. 15
      app/templates/common/html/footer.pug
  22. 2
      app/templates/common/html/header.pug
  23. 19
      app/templates/common/text/footer.pug
  24. 2
      app/templates/common/text/header.pug
  25. 3
      app/templates/html/marketing-blast.pug
  26. 13
      app/templates/html/password-reset.pug
  27. 3
      app/templates/html/user-email.pug
  28. 4
      app/templates/html/welcome.pug
  29. 121
      app/templates/layouts/html/system-message.pug
  30. 4
      app/templates/layouts/library.pug
  31. 10
      app/templates/layouts/text/system-message.pug
  32. 5
      app/templates/text/marketing-blast.pug
  33. 10
      app/templates/text/password-reset.pug
  34. 5
      app/templates/text/user-email.pug
  35. 7
      app/templates/text/welcome.pug
  36. 6
      app/views/home.pug
  37. 6
      app/views/layout/main.pug
  38. 20
      app/views/welcome/home.pug
  39. 0
      app/views/welcome/login.pug
  40. 30
      app/views/welcome/signup.pug
  41. 39
      client/js/chat-client.js
  42. 14
      config/job-queues.js
  43. 218
      config/limiter.js
  44. 22
      config/reserved-names.js
  45. 195
      dtp-chat.js
  46. 22
      package.json
  47. 6
      webpack.config.js
  48. 412
      yarn.lock

2
README.md

@ -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.

13
app/controllers/home.js

@ -10,20 +10,19 @@ import { SiteController } from '../../lib/site-controller.js';
export default class HomeController extends SiteController { export default class HomeController extends SiteController {
static get isHome ( ) { return true; } static get name ( ) { return 'HomeController'; }
static get slug ( ) { return 'home'; } static get slug ( ) { return 'home'; }
static get className ( ) { return 'HomeController'; }
constructor (dtp) {
super(dtp, HomeController.slug);
this.dtp = dtp;
}
static create (dtp) { static create (dtp) {
const instance = new HomeController(dtp); const instance = new HomeController(dtp);
return instance; return instance;
} }
constructor (dtp) {
super(dtp, HomeController.slug);
this.dtp = dtp;
}
async start ( ) { async start ( ) {
const router = express.Router(); const router = express.Router();
this.dtp.app.use('/', router); this.dtp.app.use('/', router);

73
app/controllers/manifest.js

@ -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);
}
}
}

71
app/controllers/welcome.js

@ -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');
}
}

18
app/models/auth-token.js

@ -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);

33
app/models/email-blacklist.js

@ -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);

19
app/models/email-log.js

@ -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);

17
app/models/email-verify.js

@ -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);

20
app/models/email.js

@ -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);

13
app/models/user.js

@ -11,10 +11,7 @@ const UserFlagsSchema = new Schema({
isAdmin: { type: Boolean, default: false, required: true }, isAdmin: { type: Boolean, default: false, required: true },
isModerator: { type: Boolean, default: false, required: true }, isModerator: { type: Boolean, default: false, required: true },
isEmailVerified: { type: Boolean, default: false, required: true }, isEmailVerified: { type: Boolean, default: false, required: true },
isCloaked: { type: Boolean, default: false, required: true, select: false }, isCloaked: { type: Boolean, default: false, required: true },
hasPublicProfile: { type: Boolean, default: true, required: true },
hasPublicNetwork: { type: Boolean, default: true, required: true },
requireFollowRequest: { type: Boolean, default: false, required: true },
}); });
const UserPermissionsSchema = new Schema({ const UserPermissionsSchema = new Schema({
@ -22,9 +19,7 @@ const UserPermissionsSchema = new Schema({
canChat: { type: Boolean, default: true, required: true }, canChat: { type: Boolean, default: true, required: true },
canComment: { type: Boolean, default: true, required: true }, canComment: { type: Boolean, default: true, required: true },
canReport: { type: Boolean, default: true, required: true }, canReport: { type: Boolean, default: true, required: true },
canPostStatus: { type: Boolean, default: true, required: true },
canShareLinks: { type: Boolean, default: true, required: true }, canShareLinks: { type: Boolean, default: true, required: true },
canEnablePublicProfile: { type: Boolean, default: true, required: true },
}); });
const UserOptInSchema = new Schema({ const UserOptInSchema = new Schema({
@ -32,6 +27,11 @@ const UserOptInSchema = new Schema({
marketing: { type: Boolean, default: true, required: true }, marketing: { type: Boolean, default: true, required: true },
}); });
const ProfileBadgeSchema = new Schema({
name: { type: String },
color: { type: String },
});
const UserSchema = new Schema({ const UserSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1 }, created: { type: Date, default: Date.now, required: true, index: -1 },
email: { type: String, required: true, lowercase: true, unique: true }, email: { type: String, required: true, lowercase: true, unique: true },
@ -48,6 +48,7 @@ const UserSchema = new Schema({
flags: { type: UserFlagsSchema, default: { }, required: true, select: false }, flags: { type: UserFlagsSchema, default: { }, required: true, select: false },
permissions: { type: UserPermissionsSchema, default: { }, required: true, select: false }, permissions: { type: UserPermissionsSchema, default: { }, required: true, select: false },
optIn: { type: UserOptInSchema, default: { }, required: true, select: false }, optIn: { type: UserOptInSchema, default: { }, required: true, select: false },
badges: { type: [ProfileBadgeSchema] },
lastAnnouncement: { type: Date }, lastAnnouncement: { type: Date },
membership: { type: Schema.ObjectId, index: 1, ref: 'Membership' }, membership: { type: Schema.ObjectId, index: 1, ref: 'Membership' },
paymentTokens: { paymentTokens: {

84
app/services/auth-token.js

@ -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 });
}
}

84
app/services/cache.js

@ -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);
});
});
}
}

61
app/services/crypto.js

@ -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');
}
}

315
app/services/email.js

@ -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);
}
}

64
app/services/job-queue.js

@ -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()
;
}
}

77
app/services/lib/edit-with-vi.js

@ -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,
};

64
app/services/limiter.js

@ -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;
}
}

98
app/services/session.js

@ -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);
}
}
}

71
app/services/text.js

@ -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);
}
}

664
app/services/user.js

@ -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;
}
}

15
app/templates/common/html/footer.pug

@ -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

2
app/templates/common/html/header.pug

@ -0,0 +1,2 @@
.common-title= emailTitle || `Greetings from ${site.name}!`
.common-slogan= site.description

19
app/templates/common/text/footer.pug

@ -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}
|

2
app/templates/common/text/header.pug

@ -0,0 +1,2 @@
| Dear #{recipient.displayName || recipient.username},
|

3
app/templates/html/marketing-blast.pug

@ -0,0 +1,3 @@
extends ../layouts/html/system-message
block message-body
.content-message!= htmlMessage

13
app/templates/html/password-reset.pug

@ -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

3
app/templates/html/user-email.pug

@ -0,0 +1,3 @@
extends ../layouts/html/system-message
block message-body
.content-message!= htmlMessage

4
app/templates/html/welcome.pug

@ -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.

121
app/templates/layouts/html/system-message.pug

@ -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

4
app/templates/layouts/library.pug

@ -0,0 +1,4 @@
-
function formatCount (count) {
return numeral(count).format((count > 1000) ? '0,0.0a' : '0,0');
}

10
app/templates/layouts/text/system-message.pug

@ -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

5
app/templates/text/marketing-blast.pug

@ -0,0 +1,5 @@
extends ../layouts/text/system-message
block content
|
| #{textMessage}
|

10
app/templates/text/password-reset.pug

@ -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}`}.
|

5
app/templates/text/user-email.pug

@ -0,0 +1,5 @@
extends ../layouts/text/system-message
block content
|
| #{textMessage}
|

7
app/templates/text/welcome.pug

@ -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.
|

6
app/views/home.pug

@ -1,13 +1,9 @@
include layout/main include layout/main
block view-content block view-content
section.uk-section.uk-section-default section.uk-section.uk-section-default.uk-section-small
.uk-container .uk-container
h1= site.name h1= site.name
div= site.description div= site.description
p The app is doing something else. p The app is doing something else.
p
span
i.fas.fa-home

6
app/views/layout/main.pug

@ -5,7 +5,7 @@ html(lang='en', data-obs-widget= obsWidget)
meta(name='viewport', content='width=device-width, initial-scale=1.0') meta(name='viewport', content='width=device-width, initial-scale=1.0')
meta(name='description', content= pageDescription || site.description) meta(name='description', content= pageDescription || site.description)
title= pageTitle ? `${pageTitle} | ${site.name}` : site.name title= pageTitle ? `${pageTitle} | ${site.name}` : `${site.name} | ${site.description}`
meta(name="robots", content= "index,follow") meta(name="robots", content= "index,follow")
meta(name="googlebot", content= "index,follow") meta(name="googlebot", content= "index,follow")
@ -66,7 +66,7 @@ html(lang='en', data-obs-widget= obsWidget)
) )
block view-navbar block view-navbar
nav(uk-navbar).uk-navbar-container nav(style="background: #000000;", uk-navbar).uk-navbar-container.uk-light
.uk-navbar-left .uk-navbar-left
ul.uk-navbar-nav ul.uk-navbar-nav
li.uk-active li.uk-active
@ -77,7 +77,7 @@ html(lang='en', data-obs-widget= obsWidget)
.uk-navbar-right .uk-navbar-right
ul.uk-navbar-nav ul.uk-navbar-nav
li li
a(href="/welcome/sign-up") SIGN UP a(href="/welcome/signup") SIGN UP
li li
a(href="/welcome/login") LOGIN a(href="/welcome/login") LOGIN

20
app/views/welcome/home.pug

@ -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
app/views/welcome/login.pug

30
app/views/welcome/signup.pug

@ -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

39
client/js/chat-client.js

@ -4,8 +4,41 @@
'use strict'; 'use strict';
export class ChatApp { const DTP_COMPONENT_NAME = 'DtpChatApp';
constructor ( ) {
console.log('DTP app client online'); import DtpApp from 'dtp/dtp-app.js';
export class ChatApp extends DtpApp {
constructor (user) {
super(DTP_COMPONENT_NAME, user, { withAdvertising: false });
this.log.info('DTP app client online');
}
async confirmNavigation (event) {
const target = event.currentTarget || event.target;
event.preventDefault();
event.stopPropagation();
const href = target.getAttribute('href');
const hrefTarget = target.getAttribute('target');
const text = target.textContent;
const whitelist = [
'digitaltelepresence.com',
'www.digitaltelepresence.com',
'chat.digitaltelepresence.com',
'sites.digitaltelepresence.com',
];
try {
const url = new URL(href);
if (!whitelist.includes(url.hostname)) {
await UIkit.modal.confirm(`<p>You are navigating to <code>${href}</code>, a link or button that was displayed as:</p><p>${text}</p><div class="uk-text-small uk-text-danger">Please only open links to destinations you trust and want to visit.</div>`);
}
window.open(href, hrefTarget);
} catch (error) {
this.log.info('confirmNavigation', 'navigation canceled', { error });
}
return true;
} }
} }

14
config/job-queues.js

@ -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,
},
};

218
config/limiter.js

@ -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',
},
};

22
config/reserved-names.js

@ -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',
];

195
dtp-chat.js

@ -7,13 +7,13 @@
import 'dotenv/config'; import 'dotenv/config';
import path, { dirname } from 'path'; import path, { dirname } from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line
import { createRequire } from 'module'; import { createRequire } from 'module';
const require = createRequire(import.meta.url); // jshint ignore:line const require = createRequire(import.meta.url); // jshint ignore:line
import * as glob from 'glob';
import * as rfs from 'rotating-file-stream'; import * as rfs from 'rotating-file-stream';
import webpack from 'webpack'; import webpack from 'webpack';
@ -21,6 +21,11 @@ import webpackDevMiddleware from 'webpack-dev-middleware';
import WEBPACK_CONFIG from './webpack.config.js'; import WEBPACK_CONFIG from './webpack.config.js';
import numeral from 'numeral';
import moment from 'moment';
import { Marked } from 'marked';
import hljs from 'highlight.js';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { Redis } from 'ioredis'; import { Redis } from 'ioredis';
@ -29,6 +34,9 @@ import { SiteTripwire } from './lib/site-tripwire.js';
import express from 'express'; import express from 'express';
import morgan from 'morgan'; import morgan from 'morgan';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import RedisStore from 'connect-redis';
import { Emitter } from '@socket.io/redis-emitter'; import { Emitter } from '@socket.io/redis-emitter';
@ -39,27 +47,33 @@ const APP_CONFIG = {
class Harness { class Harness {
constructor ( ) { constructor ( ) {
this.config = { this.config = { root: __dirname };
root: __dirname,
};
this.log = new SiteLog(this, 'Harness'); this.log = new SiteLog(this, 'Harness');
this.models = [ ]; this.models = [ ];
} }
async start ( ) { async start ( ) {
await this.loadConfig();
this.log.info('loading SiteTripwire (known-malicious request protection)'); this.log.info('loading SiteTripwire (known-malicious request protection)');
this.tripwire = new SiteTripwire(this); this.tripwire = new SiteTripwire(this);
await this.tripwire.start(); await this.tripwire.start();
await this.startMongoDB(); await this.connectMongoDB();
await this.loadModels();
await this.connectRedis(); await this.connectRedis();
await this.loadModels();
await this.loadServices();
await this.startExpressJS(); await this.startExpressJS();
} }
async startMongoDB ( ) { async loadConfig ( ) {
this.config.site = (await import('./config/site.js')).default;
this.config.limiter = (await import('./config/limiter.js')).default;
this.config.jobQueues = (await import('./config/job-queues.js')).default;
}
async connectMongoDB ( ) {
try { try {
this.log.info('starting MongoDB'); this.log.info('starting MongoDB');
@ -88,10 +102,16 @@ class Harness {
} }
async loadModels ( ) { async loadModels ( ) {
const modelScripts = glob.sync(path.join(this.config.root, 'app', 'models', '*.js')); const basePath = path.join(this.config.root, 'app', 'models');
this.log.info('loading models', { count: modelScripts.length }); const entries = await fs.promises.readdir(basePath, { withFileTypes: true });
for (const modelScript of modelScripts) {
const model = (await import(modelScript)).default; this.log.info('loading models', { count: entries.length });
for (const entry of entries) {
if (!entry.isFile()) {
continue;
}
const filename = path.join(basePath, entry.name);
const model = (await import(filename)).default;
if (this.models[model.modelName]) { if (this.models[model.modelName]) {
this.log.error('model name collision', { name: model.modelName }); this.log.error('model name collision', { name: model.modelName });
process.exit(-1); process.exit(-1);
@ -175,6 +195,8 @@ class Harness {
} }
async startExpressJS ( ) { async startExpressJS ( ) {
const { session: sessionService } = this.services;
this.app = express(); this.app = express();
this.app.locals.config = APP_CONFIG; this.app.locals.config = APP_CONFIG;
@ -187,6 +209,32 @@ class Harness {
this.app.use(this.tripwire.guard.bind(this.tripwire)); this.app.use(this.tripwire.guard.bind(this.tripwire));
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
this.app.use(cookieParser());
const sessionStore = new RedisStore({ client: this.redis });
const cookieDomain = (process.env.NODE_ENV === 'production') ? process.env.DTP_SITE_DOMAIN_KEY : `dev.${process.env.DTP_SITE_DOMAIN_KEY}`;
const SESSION_DURATION = 1000 * 60 * 60 * 24 * 30; // 30 days
this.sessionOptions = {
name: `dtp.${process.env.DTP_SITE_DOMAIN_KEY}.${process.env.NODE_ENV}`,
secret: process.env.DTP_HTTP_SESSION_SECRET,
resave: false,
saveUninitialized: true,
proxy: (process.env.NODE_ENV === 'production') || (process.env.HTTP_SESSION_TRUST_PROXY === 'enabled'),
cookie: {
domain: cookieDomain,
path: '/',
httpOnly: true,
secure: process.env.HTTP_COOKIE_SECURE === 'enabled',
sameSite: process.env.HTTP_COOKIE_SAMESITE || false,
maxAge: SESSION_DURATION,
},
store: sessionStore,
};
this.app.use(session(this.sessionOptions));
/* /*
* HTTP request logging * HTTP request logging
*/ */
@ -237,12 +285,15 @@ class Harness {
this.app.use(this.webpackDevMiddleware); this.app.use(this.webpackDevMiddleware);
} }
this.app.use((req, res, next) => { this.app.use(
(req, res, next) => {
res.locals.dtp = { res.locals.dtp = {
request: req, request: req,
}; };
return next(); return next();
}); },
sessionService.middleware(),
);
await this.loadControllers(); await this.loadControllers();
@ -257,22 +308,61 @@ class Harness {
}); });
} }
async loadServices ( ) {
const basePath = path.join(this.config.root, 'app', 'services');
const entries = await fs.promises.readdir(basePath, { withFileTypes: true });
const inits = [ ];
this.services = { };
for await (const entry of entries) {
if (!entry.isFile()) {
continue;
}
try {
const ServiceClass = (await import(path.join(basePath, entry.name))).default;
this.log.info('loading service', {
script: entry.name,
name: ServiceClass.name,
slug: ServiceClass.slug,
});
const service = new ServiceClass(this);
this.services[ServiceClass.slug] = service;
inits.push(service);
} catch (error) {
this.log.error('failed to load service', { error });
throw new Error('failed to load service', { cause: error });
}
}
for await (const service of inits) {
await service.start();
}
}
async loadControllers ( ) { async loadControllers ( ) {
const scripts = glob.sync(path.join(this.config.root, 'app', 'controllers', '*.js')); const basePath = path.join(this.config.root, 'app', 'controllers');
const entries = await fs.promises.readdir(basePath, { withFileTypes: true });
const inits = [ ]; const inits = [ ];
this.controllers = { }; this.controllers = { };
for await (const script of scripts) { for await (const entry of entries) {
if (!entry.isFile()) {
continue;
}
try { try {
const file = path.parse(script); const ControllerClass = (await import(path.join(basePath, entry.name))).default;
this.log.info('loading controller', { name: file.base }); this.log.info('loading controller', {
script: entry.name,
let controller = await import(script); name: ControllerClass.name,
controller = controller.default; slug: ControllerClass.slug,
controller.instance = controller.create(this); });
const controller = new ControllerClass(this);
this.controllers[controller.slug] = controller; this.controllers[ControllerClass.slug] = controller;
inits.push(controller); inits.push(controller);
} catch (error) { } catch (error) {
this.log.error('failed to load controller', { error }); this.log.error('failed to load controller', { error });
@ -281,18 +371,71 @@ class Harness {
} }
for await (const controller of inits) { for await (const controller of inits) {
if (controller.isHome) { if (controller.name === 'HomeController') {
continue; continue;
} }
await controller.instance.start(); await controller.start();
} }
/* /*
* Start the Home controller * Start the Home controller
*/ */
await this.controllers.home.instance.start(); await this.controllers.home.start();
} }
async populateViewModel (viewModel) {
viewModel.DTP_SCRIPT_DEBUG = (process.env.NODE_ENV !== 'production');
viewModel.dtp = this;
viewModel.pkg = require(this.config.root, 'package.json');
viewModel.moment = moment;
viewModel.numeral = numeral;
viewModel.phoneNumberJS = require('libphonenumber-js');
viewModel.anchorme = require('anchorme').default;
viewModel.hljs = hljs;
viewModel.Color = require('color');
viewModel.numberToWords = require('number-to-words');
viewModel.uuidv4 = require('uuid').v4;
/*
* Set up the protected markdown renderer that will refuse to process links and images
* for security reasons.
*/
function safeImageRenderer (href, title, text) { return text; }
function safeLinkRenderer (href, title, text) { return text; }
function confirmedLinkRenderer (href, title, text) {
return `<a href="${href}" uk-tooltip="${title || 'Visit link...'}" onclick="return dtp.app.confirmNavigation(event);">${text}</a>`;
}
viewModel.tileMarkedRenderer = new Marked.Renderer();
viewModel.fullMarkedRenderer = new Marked.Renderer();
viewModel.fullMarkedRenderer.image = safeImageRenderer;
viewModel.fullMarkedRenderer.link = confirmedLinkRenderer;
viewModel.safeMarkedRenderer = new Marked.Renderer();
viewModel.safeMarkedRenderer.link = safeLinkRenderer;
viewModel.safeMarkedRenderer.image = safeImageRenderer;
viewModel.markedConfigChat = {
renderer: this.safeMarkedRenderer,
// highlight: function(code, lang) {
// const language = hljs.getLanguage(lang) ? lang : 'plaintext';
// return hljs.highlight(code, { language }).value;
// },
langPrefix: 'hljs language-', // highlight.js css expects a top-level 'hljs' class.
pedantic: false,
gfm: true,
breaks: true,
sanitize: false,
smartLists: true,
smartypants: false,
xhtml: false,
};
Marked.setOptions(viewModel.markedConfigChat);
viewModel.marked = Marked;
}
} }
(async ( ) => { (async ( ) => {

22
package.json

@ -9,7 +9,7 @@
}, },
"main": "dtp-chat.js", "main": "dtp-chat.js",
"scripts": { "scripts": {
"develop": "nodemon dtp-chat.js", "dev": "nodemon dtp-chat.js",
"build": "NODE_ENV=production yarn webpack --config webpack.config.js" "build": "NODE_ENV=production yarn webpack --config webpack.config.js"
}, },
"repository": "[email protected]:digital-telepresence/dtp-chat.git", "repository": "[email protected]:digital-telepresence/dtp-chat.git",
@ -21,9 +21,19 @@
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",
"@socket.io/redis-emitter": "^5.1.0", "@socket.io/redis-emitter": "^5.1.0",
"ansicolor": "^2.0.3", "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", "dotenv": "^16.4.5",
"email-domain-check": "^1.1.4",
"email-validator": "^2.0.4",
"express": "^4.19.2", "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", "ioredis": "^5.3.2",
"marked": "^12.0.1", "marked": "^12.0.1",
"mediasoup": "^3.13.24", "mediasoup": "^3.13.24",
@ -31,13 +41,19 @@
"mongoose": "^8.2.4", "mongoose": "^8.2.4",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.13",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"pretty-checkbox": "^3.0.3", "pretty-checkbox": "^3.0.3",
"pug": "^3.0.2", "pug": "^3.0.2",
"rotating-file-stream": "^3.2.1", "rotating-file-stream": "^3.2.1",
"shoetest": "^1.2.2",
"slug": "^9.0.0", "slug": "^9.0.0",
"socket.io": "^4.7.5", "socket.io": "^4.7.5",
"striptags": "^3.2.0" "striptags": "^3.2.0",
"unzalgo": "^3.0.0",
"uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"browser-sync": "^3.0.2", "browser-sync": "^3.0.2",

6
webpack.config.js

@ -54,6 +54,12 @@ export default {
], ],
}, },
mode: webpackMode, mode: webpackMode,
resolve: {
alias: {
dtp: path.resolve(__dirname, 'lib', 'client', 'js'),
},
extensions: ['.js'],
},
output: { output: {
filename: '[name].bundle.js', filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),

412
yarn.lock

@ -966,18 +966,6 @@
resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== 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": "@jridgewell/gen-mapping@^0.3.5":
version "0.3.5" version "0.3.5"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" 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/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@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": "@mongodb-js/saslprep@^1.1.0":
version "1.1.5" version "1.1.5"
resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.5.tgz#0c48a96c8d799e81fae311b7251aa5c1dc7c6e95" resolved "https://registry.yarnpkg.com/@mongodb-js/saslprep/-/saslprep-1.1.5.tgz#0c48a96c8d799e81fae311b7251aa5c1dc7c6e95"
@ -1025,10 +1018,35 @@
dependencies: dependencies:
sparse-bitfield "^3.0.3" sparse-bitfield "^3.0.3"
"@pkgjs/parseargs@^0.11.0": "@msgpackr-extract/[email protected]":
version "0.11.0" version "3.0.2"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== 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": "@rollup/plugin-babel@^5.2.0":
version "5.3.1" 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" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 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: ansi-styles@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 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: dependencies:
color-convert "^2.0.1" 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: [email protected], ansi-wrap@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" 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-array-buffer "^3.0.4"
is-shared-array-buffer "^1.0.2" is-shared-array-buffer "^1.0.2"
asap@~2.0.3: asap@^2.0.6, asap@~2.0.3:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== 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" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== 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: busboy@^1.0.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
@ -1801,6 +1822,13 @@ character-parser@^2.2.0:
dependencies: dependencies:
is-regex "^1.0.3" 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: chokidar@^3.5.1, chokidar@^3.5.2:
version "3.6.0" version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" 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" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== 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: color-convert@^1.9.0:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 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" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== 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]: [email protected]:
version "3.6.6" version "3.6.6"
resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.6.tgz#09eff6c55af7236e137135a72574858b6786f524" 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" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== 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]: [email protected]:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== 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]: [email protected]:
version "0.6.0" version "0.6.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
@ -2034,7 +2090,14 @@ cors@~2.8.5:
object-assign "^4" object-assign "^4"
vary "^1" 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" version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== 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" resolved "https://registry.yarnpkg.com/dev-ip/-/dev-ip-1.0.1.tgz#a76a3ed1855be7a012bb8ac16cb80f3c00dc28f0"
integrity sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A== 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: doctypes@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" 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" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
eastasianwidth@^0.2.0: drange@^1.0.2:
version "0.2.0" version "1.1.1"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" resolved "https://registry.yarnpkg.com/drange/-/drange-1.1.1.tgz#b2aecec2aab82fcef11dbbd7b9e32b83f8f6c0b8"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== integrity sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==
easy-extender@^2.3.4: easy-extender@^2.3.4:
version "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" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.716.tgz#90c229ce0af2ad3b6e54472af1200e07f10293a4"
integrity sha512-t/MXMzFKQC3UfMDpw7V5wdB/UAB8dWx4hEsy+fpPYJWW3gqh3u5T1uXp6vR+H6dGCPBxkRo+YBcapBLvbGQHRw== 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: emoji-regex@^8.0.0:
version "8.0.0" version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 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: encodeurl@~1.0.1, encodeurl@~1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 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" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== 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: express@^4.19.2:
version "4.19.2" version "4.19.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
@ -2646,14 +2754,6 @@ for-each@^0.3.3:
dependencies: dependencies:
is-callable "^1.1.3" 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: formdata-polyfill@^4.0.10:
version "4.0.10" version "4.0.10"
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" 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" 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== 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: get-symbol-description@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" 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" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== 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: glob@^7.1.1, glob@^7.1.6:
version "7.2.3" version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" 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: dependencies:
function-bind "^1.1.2" 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]: [email protected]:
version "3.8.3" version "3.8.3"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" 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" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 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: isobject@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== 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: jake@^10.8.5:
version "10.8.7" version "10.8.7"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f"
@ -3420,7 +3515,7 @@ locate-path@^5.0.0:
dependencies: dependencies:
p-locate "^4.1.0" p-locate "^4.1.0"
lodash.clone@^4.3.2: lodash.clone@^4.3.2, lodash.clone@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6"
integrity sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg== 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" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== 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" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 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: lru-cache@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@ -3479,6 +3569,11 @@ lru-cache@^6.0.0:
dependencies: dependencies:
yallist "^4.0.0" 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: magic-string@^0.25.0, magic-string@^0.25.7:
version "0.25.9" version "0.25.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
@ -3605,13 +3700,6 @@ minimatch@^5.0.1:
dependencies: dependencies:
brace-expansion "^2.0.1" 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: minimatch@~3.0.2:
version "3.0.8" version "3.0.8"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" 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" resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== 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: minizlib@^2.1.1:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" 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" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 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: multer@^1.4.5-lts.1:
version "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" 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" fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10" 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: node-releases@^2.0.14:
version "2.0.14" version "2.0.14"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== 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: nodemon-webpack-plugin@^4.8.2:
version "4.8.2" version "4.8.2"
resolved "https://registry.yarnpkg.com/nodemon-webpack-plugin/-/nodemon-webpack-plugin-4.8.2.tgz#c42e15f754a8a5645174885ad7b04396a549db16" 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" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 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: path-exists@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 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" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 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]: [email protected]:
version "0.1.7" version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== 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: picocolors@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@ -4210,6 +4342,19 @@ [email protected]:
dependencies: dependencies:
side-channel "^1.0.4" 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: randombytes@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -4388,6 +4533,11 @@ [email protected]:
debug "^2.2.0" debug "^2.2.0"
minimatch "^3.0.2" 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: rollup-plugin-terser@^7.0.0:
version "7.0.2" version "7.0.2"
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" 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" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== 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" version "7.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== 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" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 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: side-channel@^1.0.4, side-channel@^1.0.6:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" 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" resolved "https://registry.yarnpkg.com/sift/-/sift-16.0.1.tgz#e9c2ccc72191585008cf3e36fc447b2d2633a053"
integrity sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ== 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: simple-update-notifier@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" 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" resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== 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]: [email protected]:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 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" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== 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 name string-width-cjs
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 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" is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1" 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: string.prototype.matchall@^4.0.6:
version "4.0.11" version "4.0.11"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" 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-obj "^1.0.1"
is-regexp "^1.0.0" 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 name strip-ansi-cjs
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@ -4876,13 +5026,6 @@ stringify-object@^3.3.0:
dependencies: dependencies:
ansi-regex "^5.0.1" 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: strip-comments@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" 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" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f"
integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ== 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]: [email protected]:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/uid2/-/uid2-1.0.0.tgz#ef8d95a128d7c5c44defa1a3d052eecc17a06bfb" 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" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== 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: upath@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" 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" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
[email protected]: [email protected], utils-merge@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== 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: vary@^1, vary@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@ -5589,7 +5756,7 @@ [email protected]:
"@types/trusted-types" "^2.0.2" "@types/trusted-types" "^2.0.2"
workbox-core "7.0.0" 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 name wrap-ansi-cjs
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 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" string-width "^4.1.0"
strip-ansi "^6.0.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: wrappy@1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"

Loading…
Cancel
Save