@paroicms/server
Version:
The ParoiCMS server
392 lines • 13.1 kB
JavaScript
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