From 727aa3f864d8c2fe7510201857f5e5104b85edad Mon Sep 17 00:00:00 2001 From: coyotte508 Date: Sun, 29 Sep 2024 11:51:41 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Update=20users?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/config/db.ts | 4 +- apps/api/src/models/apierror.ts | 6 - apps/api/src/models/index.ts | 13 -- apps/api/src/models/user.ts | 181 +++++++++--------- apps/api/{ => src}/scripts/data/Game.json | 0 apps/api/{ => src}/scripts/data/GameInfo.json | 0 .../scripts/data/GamePreferences.json | 0 apps/api/{ => src}/scripts/data/User.json | 0 apps/api/{ => src}/scripts/data/index.ts | 0 apps/api/{ => src}/scripts/seed.spec.ts | 0 apps/api/{ => src}/scripts/seed.ts | 4 +- apps/api/src/server.ts | 4 +- apps/api/src/services/cron.ts | 14 +- apps/api/src/services/gameinfo.spec.ts | 2 +- packages/types/src/user.ts | 4 +- packages/utils/src/html-escape.ts | 12 ++ packages/utils/src/index.ts | 1 + 17 files changed, 121 insertions(+), 124 deletions(-) delete mode 100644 apps/api/src/models/apierror.ts delete mode 100644 apps/api/src/models/index.ts rename apps/api/{ => src}/scripts/data/Game.json (100%) rename apps/api/{ => src}/scripts/data/GameInfo.json (100%) rename apps/api/{ => src}/scripts/data/GamePreferences.json (100%) rename apps/api/{ => src}/scripts/data/User.json (100%) rename apps/api/{ => src}/scripts/data/index.ts (100%) rename apps/api/{ => src}/scripts/seed.spec.ts (100%) rename apps/api/{ => src}/scripts/seed.ts (93%) create mode 100644 packages/utils/src/html-escape.ts diff --git a/apps/api/src/config/db.ts b/apps/api/src/config/db.ts index 6dd1e975..ce192591 100644 --- a/apps/api/src/config/db.ts +++ b/apps/api/src/config/db.ts @@ -10,7 +10,7 @@ import { createGameInfoCollection, createGameNotificationCollection, } from "@bgs/models"; -import { createUserCollection } from "../models"; +import { createUserCollection } from "../models/user"; const client = new MongoClient(env.database.bgs.url, { directConnection: true, ignoreUndefined: true }); @@ -34,7 +34,7 @@ export const collections = { users: await createUserCollection(db), }; -if (!env.isTest && cluster.isMaster) { +if (!env.isTest && cluster.isPrimary) { await using lock = await locks.lock("db"); if (lock) { diff --git a/apps/api/src/models/apierror.ts b/apps/api/src/models/apierror.ts deleted file mode 100644 index 5a3a3199..00000000 --- a/apps/api/src/models/apierror.ts +++ /dev/null @@ -1,6 +0,0 @@ -import makeCollection from "@bgs/models/api-error"; -import mongoose from "mongoose"; - -const ApiError = mongoose.model("ApiError", makeCollection()); - -export { ApiError }; diff --git a/apps/api/src/models/index.ts b/apps/api/src/models/index.ts deleted file mode 100644 index 7ffcfe27..00000000 --- a/apps/api/src/models/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from "./apierror"; -export * from "./game"; -export * from "./gameinfo"; -export * from "./gamenotification"; -export * from "./gamepreferences"; -export * from "./images"; -export * from "./jwtrefreshtokens"; -export * from "./log"; -export * from "./migrations"; -export * from "./page"; -export * from "./roommetadata"; -export * from "./settings"; -export * from "./user"; diff --git a/apps/api/src/models/user.ts b/apps/api/src/models/user.ts index c93c18da..c2ad0d6d 100644 --- a/apps/api/src/models/user.ts +++ b/apps/api/src/models/user.ts @@ -1,5 +1,4 @@ -import type { User } from "@bgs/types"; -import assert from "assert"; +import type { Game, User } from "@bgs/types"; import bcrypt from "bcryptjs"; import crypto from "crypto"; import type { Db, Collection } from "mongodb"; @@ -7,7 +6,9 @@ import { ObjectId } from "mongodb"; import { isEmpty, pick } from "lodash"; import { env, sendmail } from "../config"; import type { JsonObject, PickDeep } from "type-fest"; -import { collections } from "../config/db"; +import { collections, db, locks } from "../config/db"; +import { htmlEscape } from "@bgs/utils"; +import { GameUtils } from "./game"; export const DEFAULT_KARMA = 75; export const MAX_KARMA = 100; @@ -103,59 +104,35 @@ export namespace UserUtils { throw new Error("User is already confirmed."); } } -} - -export async function createUserCollection(db: Db): Promise>> { - const collection = db.collection>("users"); - - await collection.createIndex({ "account.username": 1 }, { unique: true }); - await collection.createIndex({ "security.slug": 1 }, { unique: true }); - await collection.createIndex({ "security.lastIp": 1 }); - await collection.createIndex({ "account.email": 1 }, { unique: true, sparse: true }); - - await collection.createIndex({ "social.facebook": 1 }, { unique: true, sparse: true }); - await collection.createIndex({ "social.google": 1 }, { unique: true, sparse: true }); - await collection.createIndex({ "social.discord": 1 }, { unique: true, sparse: true }); - return collection; -} - -export interface UserDocument extends User { - resetKey(): string; - generateConfirmKey(): void; - confirm(key: string): Promise; - recalculateKarma(since?: Date): Promise; - sendConfirmationEmail(): Promise; - sendMailChangeEmail(newEmail: string): Promise; - sendGameNotificationEmail(): Promise; - updateGameNotification(): Promise; -} + export async function sendMailChangeEmail(user: User, newEmail: string): Promise { + if (!user.account.email) { + return; + } -schema.method("sendMailChangeEmail", function (this: UserDocument, newEmail: string) { - if (!this.email()) { - return Promise.resolve(); + await sendmail({ + from: env.noreply, + to: user.account.email, + subject: "Mail change", + html: ` +

Hello ${user.account.username},

+

We're here to send you confirmation of your email change to ${htmlEscape(newEmail)}!

+

If you didn't change your email, please contact us ASAP at ${env.contact}.`, + }); } - return sendmail({ - from: env.noreply, - to: this.email(), - subject: "Mail change", - html: ` -

Hello ${this.account.username},

-

We're here to send you confirmation of your email change to ${escape(newEmail)}!

-

If you didn't change your email, please contact us ASAP at ${env.contact}.

`, - }); -}); - -schema.method("sendGameNotificationEmail", async function (this: UserDocument) { - const free = await locks.lock("game-notification", this.id); - try { + export async function sendGameNotificationEmail(_user: User): Promise { + await using lock = await locks.lock(["game-notification", _user._id]); + // Inside the lock, reload the user - const user = await User.findById(this.id); + const user = await collections.users.findOne({ _id: _user._id }); + + if (!user) { + return; + } if (!user.settings.mailing.game.activated) { - user.meta.nextGameNotification = undefined; - await user.save(); + await collections.users.updateOne({ _id: user._id }, { $set: { "meta.nextGameNotification": undefined } }); return; } @@ -168,27 +145,31 @@ schema.method("sendGameNotificationEmail", async function (this: UserDocument) { } /* check if timer already started was present at the time of the last notification for at least one game*/ - const count = await Game.count({ - currentPlayers: { $elemMatch: { _id: user._id, timerStart: { $lt: user.meta.lastGameNotification } } }, - status: "active", - }).limit(1); + const count = await collections.games.countDocuments( + { + currentPlayers: { $elemMatch: { _id: user._id, timerStart: { $lt: user.meta.lastGameNotification } } }, + status: "active", + }, + { limit: 1 } + ); if (count > 0) { return; } - const activeGames = await Game.findWithPlayersTurn(user.id).select("-data").lean(true); + const activeGames = await GameUtils.findWithPlayersTurn(user._id) + .project, "_id" | "currentPlayers">>({ _id: 1, currentPlayers: 1 }) + .toArray(); if (activeGames.length === 0) { - user.meta.nextGameNotification = undefined; - await user.save(); + await collections.users.updateOne({ _id: user._id }, { $set: { "meta.nextGameNotification": null } }); return; } /* Check the oldest game where it's your turn */ let lastMove: Date = new Date(); for (const game of activeGames) { - const timerStart = game.currentPlayers.find((pl) => pl._id.equals(this.id))?.timerStart; + const timerStart = game.currentPlayers?.find((pl) => pl._id.equals(user._id))?.timerStart; if (timerStart && timerStart < lastMove) { lastMove = timerStart; } @@ -198,25 +179,31 @@ schema.method("sendGameNotificationEmail", async function (this: UserDocument) { const notificationDate = new Date(lastMove.getTime() + (user.settings.mailing.game.delay || 30 * 60) * 1000); if (notificationDate > new Date()) { - user.meta.nextGameNotification = notificationDate; - await user.save(); + await collections.users.updateOne( + { + _id: user._id, + }, + { + $set: { "meta.nextGameNotification": notificationDate }, + } + ); return; } const gameString = activeGames.length > 1 ? `${activeGames.length} games` : "one game"; // Send email - if (this.email() && this.security.confirmed) { + if (user.account.email && user.security.confirmed) { sendmail({ from: env.noreply, - to: this.email(), + to: user.account.email, subject: `Your turn`, html: ` -

Hello ${this.account.username}

+

Hello ${user.account.username}

It's your turn on ${gameString}, click here to see your active games.

You can also change your email settings and unsubscribe