services/UserService.jsjavascript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const errors_1 = require("../utils/errors.js");
const user_1 = __importDefault(require("../db/models/user"));
const MailService_1 = __importDefault(require("./MailService"));
const NotificationService_1 = __importDefault(require("./NotificationService"));
const FeatureFlagService_1 = __importStar(require("./FeatureFlagService"));
const { Op } = require("sequelize");
const { logger } = require("../plugins/winston");
const i18n = require("i18n");
const roundToDecimals = require("../utils/numbers");
/**
* Service for managing user entities and authentication.
* Handles CRUD operations and user-specific logic.
*
* @class UserService
*/
class UserService {
/**
* Get all users.
* @param options - Options for the query.
* @param options.includeDeleted - Whether to include soft-deleted users. Defaults to false.
* @returns {Promise<Array>} Array of user objects.
*/
async getAll(options = {}) {
logger.debug(`UserService getAll ${JSON.stringify({ options })}`);
if (options.includeDeleted) {
return user_1.default.scope("includingDeleted").findAll();
}
return user_1.default.findAll();
}
/**
* Find a deleted user by username or email.
* @param {string | string[]} identifiers - The username, email, or array of identifiers to search for.
* @returns {Promise<User | null>} The deleted user object or null if not found.
*/
async findDeletedByUsernameOrEmail(identifiers) {
const user = await this.findByUsernameOrEmail(identifiers, {
scope: ["withPasswordHash", "includingDeleted"],
});
return user?.deletedAt ? user : null;
}
/**
* Get a user by ID.
* @param {UUID} id - The user's ID.
* @returns {Promise<Object>} The user object.
*/
async findById(id) {
logger.debug(`UserService findById ${JSON.stringify({ id })}`);
const accessSharingEnabled = await FeatureFlagService_1.default.isEnabled(FeatureFlagService_1.FeatureFlagKey.ACCESS_SHARING);
if (!accessSharingEnabled) {
const user = await user_1.default.findByPk(id, { include: ["Clubs"] });
if (user) {
user.childAccess = [];
user.ownerAccess = [];
}
return user;
}
return user_1.default.findByPk(id, {
include: [
"Clubs",
{
association: "childAccess",
include: ["owner"],
},
{
association: "ownerAccess",
include: ["child"],
},
],
});
}
/**
* Find a user by username or email.
* @param {string | string[]} identifier - The username, email, or array of identifiers to search for.
* @param {Object} options - Additional options.
* @param {string | string[]} [options.scope="defaultScope"] - Scope for the query.
* @returns {User} The user object.
*/
async findByUsernameOrEmail(identifier, { scope = "defaultScope" } = {}) {
const identifiers = Array.isArray(identifier) ? identifier : [identifier];
logger.debug(`UserService findByUsernameOrEmail ${JSON.stringify({
identifiers,
scope,
})}`);
const conditions = identifiers.flatMap((id) => [
{ username: id },
{ email: id },
]);
return user_1.default.scope(scope).findOne({
where: { [Op.or]: conditions },
}); // njsscan-ignore: node_nosqli_injection
}
/**
* Get the total count of users.
* @returns {Promise<number>} The total count of users.
*/
getCount() {
logger.debug(`UserService getCount`);
return user_1.default.count();
}
/**
* Get the trend of user creation and deletion.
* @returns {Promise<number>} The trend value.
*/
getTrend() {
logger.debug(`UserService getTrend`);
return Promise.all([
user_1.default.count({
where: {
createdAt: {
[Op.gt]: new Date().valueOf() - 1000 * 60 * 60 * 24 * 30,
},
},
}),
user_1.default.count({
where: {
deletedAt: {
[Op.gt]: new Date().valueOf() - 1000 * 60 * 60 * 24 * 30,
},
},
}),
]).then(([created, deleted]) => created - deleted);
}
/**
* Create a new user.
* @param {string} username - The username of the user.
* @param {string} password - The password of the user.
* @param {string} email - The email of the user.
* @param {boolean} emailConfirmed - Whether the email is confirmed.
* @param {string} locale - The locale for notifications.
* @returns {Promise<Object>} The created user object.
*/
async create(username, password, email, emailConfirmed, locale) {
logger.debug(`UserService create ${JSON.stringify({
username,
password: password ? "<redacted>" : "undefined",
email,
emailConfirmed,
locale,
})}`);
return user_1.default.create({ username, password, email, emailConfirmed }).then((user) => {
MailService_1.default.sendUserRegistrationNotice(user.username, user.id, email).catch(logger.error);
NotificationService_1.default.createOne(i18n.__({ phrase: "notifications.welcome.title", locale }), i18n.__({ phrase: "notifications.welcome.message", locale }), user.id);
if (email) {
MailService_1.default.sendEmailConfirmationEmail(user.username, user.id, email, locale).catch(logger.error);
MailService_1.default.sendWelcomeEmail(user.username, user.id, email, locale).catch(logger.error);
}
return user;
});
}
/**
* Find or create a user.
* @param {string} username - The username of the user.
* @param {string} password - The password of the user.
* @returns {Promise<Array<Object|boolean>>} The user object and a boolean indicating if the user was created.
*/
async findOrCreate(username, password) {
logger.debug(`UserService findOrCreate ${JSON.stringify({ username, password })}`);
const [user, created] = await user_1.default.findOrCreate({
where: { username },
defaults: {
username,
password,
},
});
return [user, created];
}
/**
* Generate a username candidate for a social login user.
*
* @param provider - The provider used for social login.
* @param displayName - The display name returned by the provider.
* @returns A sanitized username suitable for creation.
*/
generateSocialUsername(provider, displayName) {
logger.debug(`UserService generateSocialUsername provider=${provider} displayName=${displayName}`);
const base = displayName
? displayName.replace(/[^a-zA-Z0-9_]/g, "").slice(0, 40)
: `${provider}_${Math.random().toString(36).slice(2, 10)}`;
const padded = base.length < 6 ? `${base}${"0".repeat(6 - base.length)}` : base;
return padded;
}
/**
* Find an existing social user or create a new one from OAuth profile data.
*
* @param provider - The provider used for social login.
* @param socialId - The unique provider identifier for the user.
* @param email - The email address returned by the provider.
* @param displayName - The display name returned by the provider.
* @returns The existing or newly created user.
*/
async findOrCreateSocialUser(provider, socialId, email, displayName, locale = "en") {
logger.debug(`UserService findOrCreateSocialUser provider=${provider} socialId=${socialId} email=${email} displayName=${displayName}`);
const existing = await user_1.default.findOne({ where: { provider, socialId } });
if (existing) {
logger.debug(`Existing social user found provider=${provider} socialId=${socialId}`);
await existing.update({ lastLoggedIn: new Date() });
return existing;
}
const baseUsername = this.generateSocialUsername(provider, displayName);
let username = baseUsername;
while (await user_1.default.findOne({ where: { username } })) {
const suffix = Math.floor(Math.random() * 9999) + 1;
username = `${baseUsername}-${suffix}`;
}
if (!email) {
logger.warn(`Social user registration without email for provider=${provider} socialId=${socialId}`);
}
try {
const user = await user_1.default.create({
username,
email: email ?? undefined,
provider,
socialId,
password: undefined,
emailConfirmed: true,
});
logger.debug(`Created social user provider=${provider} socialId=${socialId} username=${username}`);
MailService_1.default.sendUserRegistrationNotice(user.username, user.id, email ?? "").catch(logger.error);
NotificationService_1.default.createOne(i18n.__({ phrase: "notifications.welcome.title", locale }), i18n.__({ phrase: "notifications.welcome.message", locale }), user.id);
if (email) {
MailService_1.default.sendWelcomeEmail(user.username, user.id, email, locale).catch(logger.error);
}
return user;
}
catch (error) {
logger.error(`Failed to create social user provider=${provider} socialId=${socialId}: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Update a user.
* @param {UUID} id - The ID of the user.
* @param {Object} data - The data to update.
* @returns {Promise<Object>} The updated user object.
*/
async update(id, data) {
const { password, ...logData } = data;
logger.debug(`UserService update ${JSON.stringify({ id, data: logData })}`);
return user_1.default.findByPk(id).then(async (foundUser) => {
if (foundUser) {
await foundUser.update(data);
await foundUser.save();
return user_1.default.findByPk(id);
}
else {
logger.error(`No user found with ID ${id} when updating`);
throw new errors_1.NotFoundError(`No user found with ID ${id} when updating`);
}
});
}
/**
* Remove a user.
* @param {UUID} id - The ID of the user.
* @returns {Promise<void>} Resolves when the user is removed.
*/
async remove(id) {
logger.debug(`UserService remove ${JSON.stringify({ id })}`);
return user_1.default.findByPk(id).then((foundUser) => {
if (foundUser) {
return foundUser.destroy();
}
else {
logger.error(`No user found with ID ${id} when deleting`);
throw new errors_1.NotFoundError(`No user found with ID ${id} when deleting`);
}
});
}
/**
* Restore a soft-deleted user.
* @param {UUID} id - The ID of the user.
* @returns {Promise<void>} Resolves when the user is restored.
*/
async restore(id) {
logger.debug(`UserService restore ${JSON.stringify({ id })}`);
return user_1.default.scope("includingDeleted")
.findByPk(id)
.then((foundUser) => {
if (foundUser) {
if (foundUser.deletedAt) {
return foundUser.restore();
}
logger.warn(`User with ID ${id} is not deleted`);
return;
}
else {
logger.error(`No user found with ID ${id} when restoring`);
throw new errors_1.NotFoundError(`No user found with ID ${id} when restoring`);
}
});
}
/**
* Get the percentage of logged-in users.
* @returns {Promise<number>} The percentage of logged-in users.
*/
getLoggedInPercentage() {
logger.debug(`UserService getLoggedInPercentage`);
return Promise.all([
this.getCount(),
user_1.default.count({
where: {
lastLoggedIn: { [Op.is]: null },
},
}),
]).then(([totalCount, notLoggedInCount]) => {
return roundToDecimals(((totalCount - notLoggedInCount) / totalCount) * 100, 1);
});
}
}
exports.default = new UserService();