UNPKG

@paroicms/server

Version:
392 lines 13.1 kB
import { makeSecret } from "@paroicms/internal-server-lib"; import { ApiError } from "@paroicms/public-server-lib"; import { type } from "arktype"; import { formatAccount, formatAuthenticatedAccount, } from "../../common/data-format.js"; import { simpleI18n } from "../../context.js"; import { comparePassword, hashPassword } from "../../helpers/passwordEncrypt-helper.js"; import { executeHook } from "../../plugin-services/make-backend-plugin-service.js"; import { updateAccountRoles } from "./account-role.queries.js"; const AccountRowAT = type({ id: "number", email: "string", name: "string|null", preferences: "string|null", passwordResetToken: "string|null", loginMethod: "string|null", active: "0|1", "+": "reject", }).pipe((r) => ({ id: String(r.id), email: r.email, name: r.name ?? undefined, preferences: r.preferences ?? undefined, passwordResetToken: r.passwordResetToken ?? undefined, loginMethod: formatInteractiveLoginMethod(r.loginMethod), active: r.active === 1, })); const FindAccountByIdAndEmailRowAT = type({ id: "number", email: "string", name: "string|null", preferences: "string|null", loginMethod: "string|null", active: "0|1", "+": "reject", }).pipe((r) => ({ id: String(r.id), email: r.email, name: r.name ?? undefined, preferences: r.preferences ?? undefined, loginMethod: formatInteractiveLoginMethod(r.loginMethod), active: r.active === 1, })); export async function findAccountByIdAndEmail(siteContext, payload) { const normalizedEmail = payload.email.trim().toLowerCase(); const account = await siteContext .cn("PaAccount as a") .select("a.id", "a.email", "a.name", "a.preferences", "a.loginMethod", "a.active") .where({ "a.id": payload.id, "a.email": normalizedEmail, }) .first(); if (!account) throw new ApiError("Account not found", 404); return FindAccountByIdAndEmailRowAT.assert(account); } const FindAccountByEmailRowAT = type({ id: "number", email: "string", name: "string|null", preferences: "string|null", passwordHash: "string|null", loginMethod: "string|null", active: "0|1", "+": "reject", }).pipe((r) => ({ id: String(r.id), email: r.email, name: r.name ?? undefined, preferences: r.preferences ?? undefined, passwordHash: r.passwordHash ?? undefined, loginMethod: formatInteractiveLoginMethod(r.loginMethod), active: r.active === 1, })); export async function findAccountByEmail(siteContext, email) { const normalizedEmail = email.trim().toLowerCase(); const account = await siteContext .cn("PaAccount as a") .select([ "a.id", "a.email", "a.name", "a.preferences", "a.passwordHash", "a.loginMethod", "a.active", ]) .where("a.email", normalizedEmail) .first(); if (!account) return; return FindAccountByEmailRowAT.assert(account); } const GetAccountRowAT = type({ id: "number", email: "string", name: "string|null", passwordResetToken: "string|null", loginMethod: "string|null", active: "0|1", "+": "reject", }).pipe((r) => ({ id: String(r.id), email: r.email, name: r.name ?? undefined, passwordResetToken: r.passwordResetToken ?? undefined, loginMethod: formatInteractiveLoginMethod(r.loginMethod), active: r.active === 1, })); export async function getAccount(siteContext, id) { const found = await siteContext .cn("PaAccount as a") .select(["a.id", "a.email", "a.name", "a.passwordResetToken", "a.loginMethod", "a.active"]) .where("a.id", id) .first(); if (!found) throw new ApiError("Account not found", 404); const account = GetAccountRowAT.assert(found); return formatAccount(account); } export async function getAuthenticatedAccount(siteContext, id) { const found = await siteContext .cn("PaAccount as a") .select([ "a.id", "a.email", "a.name", "a.preferences", "a.passwordResetToken", "a.loginMethod", "a.active", ]) .where("a.id", id) .first(); if (!found) throw new ApiError("Account not found", 404); const account = AccountRowAT.assert(found); return formatAuthenticatedAccount(account); } export async function getAllAccounts(siteContext) { const accounts = await siteContext .cn("PaAccount as a") .select([ "a.id", "a.email", "a.name", "a.preferences", "a.passwordResetToken", "a.loginMethod", "a.active", ]); const parsedAccounts = accounts .map((account) => { return AccountRowAT.assert(account); }) .map((user) => formatAccount(user)); return parsedAccounts; } export async function setAccountPreferences(siteContext, accountId, values) { await siteContext .cn("PaAccount") .where({ id: accountId }) .update({ preferences: JSON.stringify(values) }); } export async function updateAccountLoginMethod(siteContext, accountId, loginMethod) { await siteContext.cn("PaAccount").where({ id: accountId }).update({ loginMethod }); } export async function updateAccountActive(siteContext, accountId, active) { await siteContext .cn("PaAccount") .where({ id: accountId }) .update({ active: active ? 1 : 0 }); } const CreateAccountInsertedAT = type({ id: "number", "+": "reject", }).pipe((r) => ({ id: String(r.id), })); export async function createAccount(siteContext, payload) { const normalizedEmail = payload.email.trim().toLowerCase(); if (!normalizedEmail) throw new ApiError("email is required", 400); const account = await findAccountByEmail(siteContext, normalizedEmail); if (account) throw new ApiError("email already exists", 409); const [inserted] = await siteContext .cn("PaAccount") .insert({ email: normalizedEmail, name: payload.name, preferences: JSON.stringify({ language: payload.language }), }) .returning("id"); const id = CreateAccountInsertedAT.assert(inserted).id; const newAccount = await findAccountById(siteContext, id); if (!newAccount) { throw new ApiError("Failed to get last insert account", 500); } if (payload.roles.length > 0) { await updateAccountRoles(siteContext, id, payload.roles); } if (payload.accountType === "google") { return await createGoogleAccount(siteContext, { payload, newAccount, }); } await resetAccountToken(siteContext, { id: newAccount.id, email: newAccount.email, language: payload.language, }); return formatAccount(newAccount); } export async function insertSpecialAccount(siteContext, payload) { const CreateAccountInsertedAT = type({ id: "number", "+": "reject", }).pipe((r) => ({ id: String(r.id), })); const normalizedEmail = payload.email.trim().toLowerCase(); const [inserted] = await siteContext .cn("PaAccount") .insert({ email: normalizedEmail, name: payload.name, loginMethod: payload.loginMethod, active: 1, }) .returning("id"); return CreateAccountInsertedAT.assert(inserted).id; } async function createGoogleAccount(siteContext, { payload, newAccount, }) { const subject = simpleI18n.translate({ key: "accountCreation.subject", language: payload.language, }); const message = simpleI18n.translate({ key: "accountCreation.message", language: payload.language, args: [`${siteContext.siteUrl}/adm`], }); await executeHook(siteContext, "sendMail", { value: { subject: `${subject} - ${siteContext.fqdn}`, html: message, to: newAccount.email, }, }); return formatAccount(newAccount); } export async function updateAccount(siteContext, accountId, payload) { const account = await findAccountById(siteContext, accountId); if (!account) throw new ApiError("Account not found", 404); const updatePayload = { ...payload, email: payload.email ? payload.email.trim().toLowerCase() : undefined, }; await siteContext.cn("PaAccount").where({ id: account.id }).update(updatePayload); return formatAccount({ ...account, email: updatePayload.email ?? account.email, name: payload.name ?? account.name, }); } export async function deleteAccount(siteContext, accountId) { const account = await findAccountById(siteContext, accountId); if (!account) throw new ApiError("Account not found", 404); const isReferenced = await isAccountReferenced(siteContext, accountId); if (isReferenced) { await updateAccountActive(siteContext, accountId, false); } else { await siteContext.cn("PaAccount").where({ id: account.id }).delete(); } } export async function isAccountReferenced(siteContext, accountId) { const eventCount = await siteContext .cn("PaEventLog") .where("actorId", accountId) .count("* as count") .first(); return !!eventCount && eventCount.count > 0; } export async function resetAccountPassword(siteContext, accountId) { const account = await findAccountById(siteContext, accountId); if (!account) throw new ApiError("Account not found", 404); const language = getAccountLanguage(siteContext, account); await resetAccountToken(siteContext, { id: account.id, email: account.email, language }); } export async function changeOwnPassword(siteContext, accountId, currentPassword, newPassword) { if (!newPassword) throw new ApiError("New password cannot be empty", 400); const account = await findAccountById(siteContext, accountId); if (!account) throw new ApiError("Account not found", 404); if (!account.active) throw new ApiError("Account is not active", 403); if (!account.passwordHash) throw new ApiError("Account has no password", 400); const isMatch = await comparePassword(currentPassword, account.passwordHash); if (!isMatch) throw new ApiError("Current password is incorrect", 401); const newHash = await hashPassword(newPassword); await siteContext .cn("PaAccount") .where({ id: accountId }) .update({ passwordHash: newHash, passwordResetToken: null }); } const FindAccountByIdRowAT = type({ id: "number", email: "string", name: "string|null", preferences: "string|null", passwordHash: "string|null", passwordResetToken: "string|null", active: "0|1", "+": "reject", }).pipe((data) => ({ id: String(data.id), email: data.email, name: data.name ?? undefined, preferences: data.preferences ?? undefined, passwordHash: data.passwordHash ?? undefined, passwordResetToken: data.passwordResetToken ?? undefined, active: data.active === 1, })); async function findAccountById(siteContext, id) { const found = await siteContext .cn("PaAccount as a") .select([ "a.id", "a.email", "a.name", "a.preferences", "a.passwordHash", "a.passwordResetToken", "a.active", ]) .where("a.id", id) .first(); if (!found) return; return FindAccountByIdRowAT.assert(found); } async function resetAccountToken(siteContext, account) { const passwordResetToken = makeSecret(60); await siteContext.cn("PaAccount").where({ id: account.id }).update({ passwordResetToken, passwordHash: null, }); const subject = simpleI18n.translate({ key: "accountPasswordReset.subject", language: account.language, }); const resetUrl = `${siteContext.siteUrl}/adm/?account=${account.id}&reset-password=${encodeURIComponent(passwordResetToken)}`; const message = simpleI18n.translate({ key: "accountPasswordReset.message", language: account.language, args: [resetUrl], }); await executeHook(siteContext, "sendMail", { value: { subject: `${subject} - ${siteContext.fqdn}`, html: message, to: account.email, }, }); } function getAccountLanguage(siteContext, account) { const preferences = account.preferences ? JSON.parse(account.preferences) : undefined; return preferences?.language ?? siteContext.siteSchema.defaultLanguage; } function formatInteractiveLoginMethod(loginMethod) { if (!loginMethod) return; switch (loginMethod) { case "local": case "localDev": case "platform": case "platformAdmin": return loginMethod; default: throw new Error(`Invalid login method "${loginMethod}"`); } } //# sourceMappingURL=account.queries.js.map