Added account update form and e-mail verification.
This commit is contained in:
@@ -3,11 +3,10 @@ import { CronJob } from 'cron';
|
||||
import { add } from 'date-fns';
|
||||
import escapeRegexp from 'escape-string-regexp';
|
||||
import markdownIt from 'markdown-it';
|
||||
import { Resend } from 'resend';
|
||||
|
||||
import promiseProps from '../utils/promise-props.js';
|
||||
import { getIdsBySlug } from './cache.js';
|
||||
import { emailTemplates } from './email-templates.js';
|
||||
import { sendEmails } from './email.js';
|
||||
import { HttpError } from './errors.js';
|
||||
import { knexOwner as knex } from './knex.js';
|
||||
import initLogger from './logger.js';
|
||||
@@ -15,18 +14,6 @@ import { indexApi } from './manticore.js';
|
||||
import { fetchScenesById } from './scenes.js';
|
||||
|
||||
const logger = initLogger();
|
||||
const resend = (() => {
|
||||
if (config.email.enabled) {
|
||||
try {
|
||||
return new Resend(config.email.apiKey);
|
||||
}
|
||||
catch (error) {
|
||||
logger.error(`Failed to initialize Resend: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
function getImagePath(media) {
|
||||
if (!media) {
|
||||
@@ -452,11 +439,6 @@ async function sendEmailAlerts() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resend) {
|
||||
logger.warn('E-mails are enabled, but Resend is not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const qualifyingUsers = await knex('notifications')
|
||||
.select('users.id as user_id', 'users.settings', knex.raw('max(emailed_at) as last_email'))
|
||||
.where('notifications.email', true)
|
||||
@@ -546,7 +528,7 @@ async function sendEmailAlerts() {
|
||||
&& !!notification.scene
|
||||
&& notification.user.settings?.alertEmailsEnabled !== false); // keep the e-mails marked as 'sent', if emails get re-enabled there shouldn't be a barrage of old notifications
|
||||
|
||||
const emails = await Promise.all(Object.values(usersByUserId).map(async (user) => {
|
||||
const emailItems = Object.values(usersByUserId).map((user) => {
|
||||
const userTriggers = Object.values(Object.fromEntries(triggers
|
||||
.filter((trigger) => trigger.user.id === user.id)
|
||||
.map((trigger) => [trigger.scene.id, trigger])))
|
||||
@@ -556,42 +538,24 @@ async function sendEmailAlerts() {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const { html, text } = await emailTemplates.render('alert.vue', {
|
||||
props: {
|
||||
user,
|
||||
triggers: userTriggers,
|
||||
address: config.web.address,
|
||||
},
|
||||
});
|
||||
return {
|
||||
template: 'alert',
|
||||
to: user.email,
|
||||
subject: `Notification for ${userTriggers.length} scenes on traxxx`,
|
||||
props: {
|
||||
user,
|
||||
triggers: userTriggers,
|
||||
address: config.web.address,
|
||||
},
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
return {
|
||||
from: config.email.from,
|
||||
to: user.email,
|
||||
subject: `Notification for ${userTriggers.length} scenes on traxxx`,
|
||||
html,
|
||||
text,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
logger.warn(`Failed to render e-mail for '${user.username}' with scenes ${userTriggers.map((trigger) => `${trigger.notificationId}: ${trigger.scene.id}`)}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
})).then((allEmails) => allEmails.filter(Boolean));
|
||||
|
||||
if (emails.length === 0) {
|
||||
if (emailItems.length === 0) {
|
||||
logger.info(`No alert e-mails sent`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, error } = await resend.batch.send(emails);
|
||||
|
||||
if (error) {
|
||||
logger.error(`Failed to send ${emails.length} alert e-mails (${error.statusCode} ${error.name}): ${error.message}`);
|
||||
}
|
||||
else {
|
||||
logger.info(`Sent ${data.data.length}/${emails.length} alert e-mails`);
|
||||
}
|
||||
await sendEmails(emailItems);
|
||||
}
|
||||
|
||||
export async function notify(sceneIds, options = {}) {
|
||||
|
||||
217
src/auth.js
217
src/auth.js
@@ -1,3 +1,4 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import util from 'node:util';
|
||||
@@ -8,6 +9,7 @@ import config from 'config';
|
||||
import { verify } from 'hcaptcha';
|
||||
|
||||
import slugify from '../utils/slugify.js';
|
||||
import { sendEmails } from './email.js';
|
||||
import { HttpError } from './errors.js';
|
||||
import { knexOwner as knex } from './knex.js';
|
||||
import initLogger, { initAccessLogger } from './logger.js';
|
||||
@@ -46,7 +48,7 @@ async function generateAvatar(user) {
|
||||
|
||||
export async function login(credentials, userIp) {
|
||||
if (!config.auth.login) {
|
||||
throw new HttpError('Logins are currently disabled', 405);
|
||||
throw new HttpError('Logins are currently disabled', 503);
|
||||
}
|
||||
|
||||
const { user } = await fetchUser(credentials.username.trim(), {
|
||||
@@ -76,9 +78,62 @@ export async function login(credentials, userIp) {
|
||||
return curateUser(user, user);
|
||||
}
|
||||
|
||||
async function hashPassword(password) {
|
||||
const salt = crypto.randomBytes(16).toString('hex');
|
||||
const hashedPassword = (await scrypt(password, salt, 64)).toString('hex');
|
||||
const storedPassword = `${salt}/${hashedPassword}`;
|
||||
|
||||
return {
|
||||
storedPassword,
|
||||
hashedPassword,
|
||||
salt,
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendEmailVerification(user, customEmail, trigger) {
|
||||
if (!user) {
|
||||
throw new HttpError('No user provided', 401);
|
||||
}
|
||||
|
||||
const email = customEmail || user.email;
|
||||
|
||||
if (!email) {
|
||||
throw new HttpError('No e-mail address provided', 400);
|
||||
}
|
||||
|
||||
const tokenKey = `traxxx:token_email:${user.id}`;
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
const tokenData = {
|
||||
email,
|
||||
token,
|
||||
};
|
||||
|
||||
await redis.set(tokenKey, JSON.stringify(tokenData));
|
||||
await redis.expire(tokenKey, config.auth.emailTokenExpiry * 60);
|
||||
|
||||
const verifyUrl = `${config.web.address}/verify/email?${new URLSearchParams({
|
||||
userId: user.id,
|
||||
token,
|
||||
}).toString()}`;
|
||||
|
||||
logger.info(`E-mail verification sent to ${user.username} (${user.id}): ${verifyUrl}`);
|
||||
|
||||
await sendEmails([{
|
||||
template: 'verify',
|
||||
to: email,
|
||||
subject: 'Please verify your traxxx e-mail address',
|
||||
props: {
|
||||
user,
|
||||
verifyUrl,
|
||||
trigger,
|
||||
},
|
||||
}]);
|
||||
}
|
||||
|
||||
export async function signup(credentials, userIp) {
|
||||
if (!config.auth.signup) {
|
||||
throw new HttpError('Sign-ups are currently disabled', 405);
|
||||
throw new HttpError('Sign-ups are currently disabled', 503);
|
||||
}
|
||||
|
||||
const curatedUsername = credentials.username.trim();
|
||||
@@ -125,9 +180,7 @@ export async function signup(credentials, userIp) {
|
||||
throw new HttpError('Username or e-mail already in use', 409);
|
||||
}
|
||||
|
||||
const salt = crypto.randomBytes(16).toString('hex');
|
||||
const hashedPassword = (await scrypt(credentials.password, salt, 64)).toString('hex');
|
||||
const storedPassword = `${salt}/${hashedPassword}`;
|
||||
const { storedPassword } = await hashPassword(credentials.password);
|
||||
|
||||
const [{ id: userId }] = await knex('users')
|
||||
.insert({
|
||||
@@ -152,7 +205,159 @@ export async function signup(credentials, userIp) {
|
||||
username: curatedUsername,
|
||||
});
|
||||
|
||||
return fetchUser(userId);
|
||||
const user = await fetchUser(userId, {
|
||||
includePrivate: true,
|
||||
});
|
||||
|
||||
await sendEmailVerification(user, null, 'signup');
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function updatePassword(password, reqUser, reqSession) {
|
||||
const now = new Date();
|
||||
const { storedPassword } = await hashPassword(password);
|
||||
|
||||
await knex('users')
|
||||
.where('id', reqUser.id)
|
||||
.update('password', storedPassword);
|
||||
|
||||
const country = reqSession.country && await knex('countries').where('alpha2', reqSession.country).first();
|
||||
|
||||
await sendEmails([{
|
||||
template: 'password',
|
||||
to: reqUser.email,
|
||||
subject: 'Your traxxx password has been changed',
|
||||
props: {
|
||||
user: reqUser,
|
||||
ip: reqUser.ip,
|
||||
country: country?.name,
|
||||
address: config.web.address,
|
||||
timestamp: now,
|
||||
},
|
||||
}]);
|
||||
|
||||
logger.info(`Password changed by ${reqUser.username} (${reqUser.id}, ${reqUser.ip})`);
|
||||
}
|
||||
|
||||
async function updateEmail(email, reqUser) {
|
||||
const user = await fetchUser(email, {
|
||||
email: true,
|
||||
throwError: false,
|
||||
});
|
||||
|
||||
if (user) {
|
||||
throw new HttpError('This e-mail address is already in use', 409);
|
||||
}
|
||||
|
||||
await sendEmailVerification(reqUser, email, 'change');
|
||||
}
|
||||
|
||||
export async function verifyEmail(userId, emailToken) {
|
||||
if (!userId) {
|
||||
throw new HttpError('User ID not provided', 400);
|
||||
}
|
||||
|
||||
if (!emailToken) {
|
||||
throw new HttpError('Token not provided', 400);
|
||||
}
|
||||
|
||||
const tokenKey = `traxxx:token_email:${userId}`;
|
||||
const tokenEntry = await redis.getDel(tokenKey);
|
||||
|
||||
if (!tokenEntry) {
|
||||
throw new HttpError('Invalid verification link', 401);
|
||||
}
|
||||
|
||||
const { token, email } = JSON.parse(tokenEntry);
|
||||
|
||||
if (!token
|
||||
|| !email
|
||||
|| emailToken.length !== token.length
|
||||
|| !crypto.timingSafeEqual(Buffer.from(emailToken, 'hex'), Buffer.from(token, 'hex'))) {
|
||||
throw new HttpError('Invalid verification link', 401);
|
||||
}
|
||||
|
||||
const updated = await knex('users')
|
||||
.where('id', userId)
|
||||
.update({
|
||||
email,
|
||||
email_verified: true,
|
||||
});
|
||||
|
||||
if (updated === 0) {
|
||||
throw new HttpError('E-mail verification failed because the user could not be found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateUser({ credentials, password }, reqUser, reqSession) {
|
||||
if (!reqUser) {
|
||||
throw new HttpError('You are not logged in', 401);
|
||||
}
|
||||
|
||||
if (!credentials?.email && !credentials.password) {
|
||||
throw new HttpError('No changeable credentials provided', 400);
|
||||
}
|
||||
|
||||
if (credentials.email && !config.email.enabled) {
|
||||
throw new HttpError('E-mail is currently disabled', 503);
|
||||
}
|
||||
|
||||
if (credentials.password && credentials.password?.length < 3) {
|
||||
throw new HttpError('Password must be 3 characters or longer', 400);
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
throw new HttpError('No current password provided', 401);
|
||||
}
|
||||
|
||||
const { user } = await fetchUser(reqUser.id, {
|
||||
email: true,
|
||||
raw: true,
|
||||
}).catch(() => {
|
||||
throw new HttpError('Username or password incorrect', 401);
|
||||
});
|
||||
|
||||
await verifyPassword(password, user.password);
|
||||
|
||||
if (credentials.password) {
|
||||
await updatePassword(credentials.password, reqUser, reqSession);
|
||||
}
|
||||
|
||||
if (credentials.email) {
|
||||
await updateEmail(credentials.email, reqUser);
|
||||
}
|
||||
|
||||
return curateUser(user, reqUser);
|
||||
}
|
||||
|
||||
const validSettingKeys = [
|
||||
'alertNotificationsEnabled',
|
||||
'alertEmailsEnabled',
|
||||
'alertEmailsFrequency',
|
||||
];
|
||||
|
||||
export async function updateSettings(settings, reqUser) {
|
||||
if (!reqUser) {
|
||||
throw new HttpError('You need to be authenticated to change your settings', 401);
|
||||
}
|
||||
|
||||
const invalidKeys = Object.keys(settings).filter((key) => !validSettingKeys.includes(key));
|
||||
|
||||
if (invalidKeys.length > 0) {
|
||||
throw new HttpError(`Invalid settings: ${invalidKeys.join()}`, 400);
|
||||
}
|
||||
|
||||
if (settings.alertEmailsFrequency && !['instant', 'daily', 'weekly', 'monthly'].includes(settings.alertEmailsFrequency)) {
|
||||
throw new HttpError('Invalid email frequency setting', 400);
|
||||
}
|
||||
|
||||
const [{ settings: updatedSettings }] = await knex('users')
|
||||
.where('id', reqUser.id)
|
||||
.update('settings', knex.raw('coalesce(settings, \'{}\'::jsonb) || ?::jsonb', [JSON.stringify(settings)]))
|
||||
.returning('settings');
|
||||
|
||||
return updatedSettings;
|
||||
}
|
||||
|
||||
function curateKey(key) {
|
||||
|
||||
79
src/email.js
Normal file
79
src/email.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import config from 'config';
|
||||
import { Resend } from 'resend';
|
||||
|
||||
import { emailTemplates } from './email-templates.js';
|
||||
import initLogger from './logger.js';
|
||||
|
||||
const logger = initLogger();
|
||||
|
||||
const resend = (() => {
|
||||
if (config.email.enabled) {
|
||||
try {
|
||||
return new Resend(config.email.apiKey);
|
||||
}
|
||||
catch (error) {
|
||||
logger.error(`Failed to initialize Resend: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
export async function renderEmail(template, { to, subject, props }) {
|
||||
const { html, text } = await emailTemplates.render(`${template}.vue`, { props });
|
||||
|
||||
return {
|
||||
from: config.email.from,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
// always batches, even for a single item: Resend's batch endpoint accepts a batch of one,
|
||||
// and passing batchValidation: 'permissive' means one bad recipient among many doesn't
|
||||
// sink the rest of the batch (the default 'strict' mode fails the whole call instead)
|
||||
export async function sendEmails(items) {
|
||||
if (!config.email.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!resend) {
|
||||
logger.warn('E-mails are enabled, but Resend is not initialized');
|
||||
return null;
|
||||
}
|
||||
|
||||
const rendered = await Promise.all(items.map(async (item) => {
|
||||
try {
|
||||
return await renderEmail(item.template, item);
|
||||
}
|
||||
catch (error) {
|
||||
logger.error(`Failed to render '${item.template}' e-mail for '${item.to}': ${error.message} — ${JSON.stringify(item.props)}`);
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
|
||||
const sentItems = items.filter((item, index) => rendered[index]);
|
||||
const emails = rendered.filter(Boolean);
|
||||
|
||||
if (emails.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { data, error } = await resend.batch.send(emails, { batchValidation: 'permissive' });
|
||||
|
||||
if (error) {
|
||||
logger.error(`Failed to send ${emails.length} e-mail(s) (${error.statusCode} ${error.name}): ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
data.errors?.forEach(({ index, message }) => {
|
||||
const item = sentItems[index];
|
||||
logger.error(`Failed to send '${item.template}' e-mail to '${item.to}': ${message} — ${JSON.stringify(item.props)}`);
|
||||
});
|
||||
|
||||
logger.info(`Sent ${emails.length - (data.errors?.length || 0)}/${items.length} e-mail(s)`);
|
||||
|
||||
return data;
|
||||
}
|
||||
37
src/users.js
37
src/users.js
@@ -14,7 +14,7 @@ function curateTemplate(template) {
|
||||
};
|
||||
}
|
||||
|
||||
export function curateUser(user, reqUser) {
|
||||
export function curateUser(user, reqUser, options = {}) {
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export function curateUser(user, reqUser) {
|
||||
role: user.role,
|
||||
abilities: [...user.role_abilities || [], ...user.abilities || []],
|
||||
createdAt: user.created_at,
|
||||
...(reqUser?.id === user.id ? privateUser : null),
|
||||
...(reqUser?.id === user.id || options.includePrivate ? privateUser : null),
|
||||
};
|
||||
|
||||
return curatedUser;
|
||||
@@ -63,7 +63,7 @@ export async function fetchUser(userId, options = {}, reqUser) {
|
||||
.groupBy('users.id', 'users_roles.role')
|
||||
.first();
|
||||
|
||||
if (!user) {
|
||||
if (!user && options.throwError !== false) {
|
||||
throw new HttpError(`User '${userId}' not found`, 404);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function fetchUser(userId, options = {}, reqUser) {
|
||||
return { user };
|
||||
}
|
||||
|
||||
return curateUser(user, reqUser);
|
||||
return curateUser(user, reqUser, options);
|
||||
}
|
||||
|
||||
export async function fetchUserTemplates(reqUser) {
|
||||
@@ -136,32 +136,3 @@ export async function createBan(ban, reqUser) {
|
||||
|
||||
await knex('bans').insert(curatedBan);
|
||||
}
|
||||
|
||||
const validSettingKeys = [
|
||||
'alertNotificationsEnabled',
|
||||
'alertEmailsEnabled',
|
||||
'alertEmailsFrequency',
|
||||
];
|
||||
|
||||
export async function updateSettings(settings, reqUser) {
|
||||
if (!reqUser) {
|
||||
throw new HttpError('You need to be authenticated to change your settings', 401);
|
||||
}
|
||||
|
||||
const invalidKeys = Object.keys(settings).filter((key) => !validSettingKeys.includes(key));
|
||||
|
||||
if (invalidKeys.length > 0) {
|
||||
throw new HttpError(`Invalid settings: ${invalidKeys.join()}`, 400);
|
||||
}
|
||||
|
||||
if (settings.alertEmailsFrequency && !['instant', 'daily', 'weekly', 'monthly'].includes(settings.alertEmailsFrequency)) {
|
||||
throw new HttpError('Invalid email frequency setting', 400);
|
||||
}
|
||||
|
||||
const [{ settings: updatedSettings }] = await knex('users')
|
||||
.where('id', reqUser.id)
|
||||
.update('settings', knex.raw('coalesce(settings, \'{}\'::jsonb) || ?::jsonb', [JSON.stringify(settings)]))
|
||||
.returning('settings');
|
||||
|
||||
return updatedSettings;
|
||||
}
|
||||
|
||||
@@ -219,12 +219,12 @@ export async function updateNotificationGraphql(query, req) {
|
||||
return updatedNotification;
|
||||
}
|
||||
|
||||
export const router = Router();
|
||||
export const alertsRouter = Router();
|
||||
|
||||
router.get('/api/alerts', fetchAlertsApi);
|
||||
router.post('/api/alerts', createAlertApi);
|
||||
router.delete('/api/alerts/:alertId', removeAlertApi);
|
||||
alertsRouter.get('/api/alerts', fetchAlertsApi);
|
||||
alertsRouter.post('/api/alerts', createAlertApi);
|
||||
alertsRouter.delete('/api/alerts/:alertId', removeAlertApi);
|
||||
|
||||
router.get('/api/users/:userId/notifications', fetchNotificationsApi);
|
||||
router.patch('/api/users/:userId/notifications', updateNotificationsApi);
|
||||
router.patch('/api/users/:userId/notifications/:notificationId', updateNotificationApi);
|
||||
alertsRouter.get('/api/users/:userId/notifications', fetchNotificationsApi);
|
||||
alertsRouter.patch('/api/users/:userId/notifications', updateNotificationsApi);
|
||||
alertsRouter.patch('/api/users/:userId/notifications/:notificationId', updateNotificationApi);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { Router } from 'express';
|
||||
import IPCIDR from 'ip-cidr';
|
||||
import argv from '../argv.js';
|
||||
|
||||
@@ -8,10 +9,16 @@ import {
|
||||
flushUserKeys,
|
||||
login,
|
||||
removeUserKey,
|
||||
sendEmailVerification,
|
||||
signup,
|
||||
updateSettings,
|
||||
updateUser,
|
||||
verifyEmail,
|
||||
} from '../auth.js';
|
||||
|
||||
import { fetchUser } from '../users.js';
|
||||
import {
|
||||
fetchUser,
|
||||
} from '../users.js';
|
||||
|
||||
// changes every time the process restarts, so a deploy invalidates all cached session users
|
||||
// even if they're within the staleness window below
|
||||
@@ -131,6 +138,60 @@ export async function signupApi(req, res) {
|
||||
res.send(user);
|
||||
}
|
||||
|
||||
async function updateUserApi(req, res) {
|
||||
const updatedUser = await updateUser(req.body, req.user, req.session);
|
||||
const sessions = await req.sessionStore.all();
|
||||
|
||||
req.session.user.refreshedAt = 0;
|
||||
|
||||
await Promise.all(sessions.map(async (session) => {
|
||||
if (session.user?.id === req.user.id && session.id !== req.sessionID) {
|
||||
if (req.body.credentials?.password) {
|
||||
// we changed the password, destroy all user sessions except our own (we already provided all the credentials)
|
||||
await req.sessionStore.destroy(session.id);
|
||||
}
|
||||
else {
|
||||
// mark our sessions as stale
|
||||
session.user.refreshedAt = 0; // eslint-disable-line no-param-reassign
|
||||
await req.sessionStore.set(session.id, session);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
res.send(updatedUser);
|
||||
}
|
||||
|
||||
async function sendEmailVerificationApi(req, res) {
|
||||
await sendEmailVerification(req.user, null, 'verify');
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
async function verifyEmailApi(req, res) {
|
||||
const userId = Number(req.body.userId) || null;
|
||||
|
||||
await verifyEmail(userId, req.body.token);
|
||||
|
||||
const sessions = await req.sessionStore.all();
|
||||
|
||||
await Promise.all(sessions.map(async (session) => {
|
||||
if (session.user?.id === userId) {
|
||||
session.user.refreshedAt = 0; // eslint-disable-line no-param-reassign
|
||||
await req.sessionStore.set(session.id, session);
|
||||
}
|
||||
}));
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
async function updateSettingsApi(req, res) {
|
||||
const updatedSettings = await updateSettings(req.body, req.user);
|
||||
|
||||
req.session.user.settings = updatedSettings;
|
||||
|
||||
res.send(updatedSettings);
|
||||
}
|
||||
|
||||
export async function fetchUserKeysApi(req, res) {
|
||||
const keys = await fetchUserKeys(req.user);
|
||||
|
||||
@@ -154,3 +215,28 @@ export async function flushUserKeysApi(req, res) {
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
export const authRouter = Router();
|
||||
|
||||
// SESSION
|
||||
// authRouter.use(setUserApi);
|
||||
|
||||
authRouter.post('/api/session', loginApi);
|
||||
authRouter.delete('/api/session', logoutApi);
|
||||
|
||||
// USERS
|
||||
authRouter.post('/api/users', signupApi);
|
||||
|
||||
// SETTINGS
|
||||
authRouter.patch('/api/me', updateUserApi);
|
||||
authRouter.patch('/api/me/settings', updateSettingsApi);
|
||||
|
||||
// EMAIL VERIFICATION
|
||||
authRouter.post('/api/me/email_verification', sendEmailVerificationApi);
|
||||
authRouter.post('/api/emails/verify', verifyEmailApi); // usable without session
|
||||
|
||||
// API KEYS
|
||||
authRouter.get('/api/me/keys', fetchUserKeysApi);
|
||||
authRouter.post('/api/me/keys', createKeyApi);
|
||||
authRouter.delete('/api/me/keys/:keyIdentifier', removeUserKeyApi);
|
||||
authRouter.delete('/api/me/keys', flushUserKeysApi);
|
||||
|
||||
@@ -30,6 +30,7 @@ export default async function mainHandler(req, res, next) {
|
||||
role: req.user.role,
|
||||
abilities: req.user.abilities,
|
||||
avatar: req.user.avatar,
|
||||
isEmailVerified: req.user.isEmailVerified,
|
||||
},
|
||||
assets: req.user
|
||||
? {
|
||||
|
||||
@@ -12,19 +12,11 @@ import initLogger from '../logger.js';
|
||||
|
||||
import redis from '../redis.js';
|
||||
import queryBoolean from '../utils/query-boolean.js';
|
||||
import { actorsRouter } from './actors.js';
|
||||
|
||||
import { router as alertsRouter } from './alerts.js';
|
||||
import {
|
||||
createKeyApi,
|
||||
fetchUserKeysApi,
|
||||
flushUserKeysApi,
|
||||
loginApi,
|
||||
logoutApi,
|
||||
removeUserKeyApi,
|
||||
setUserApi,
|
||||
signupApi,
|
||||
} from './auth.js';
|
||||
import { actorsRouter } from './actors.js';
|
||||
import { alertsRouter } from './alerts.js';
|
||||
import { authRouter, setUserApi } from './auth.js';
|
||||
|
||||
import consentHandler from './consent.js';
|
||||
|
||||
import { fetchEntitiesApi } from './entities.js';
|
||||
@@ -91,8 +83,6 @@ export default async function initServer() {
|
||||
store: redisStore,
|
||||
}));
|
||||
|
||||
router.use(setUserApi);
|
||||
|
||||
// Vite integration
|
||||
if (isProduction) {
|
||||
app.enable('trust proxy');
|
||||
@@ -118,6 +108,7 @@ export default async function initServer() {
|
||||
router.use(viteDevMiddleware);
|
||||
}
|
||||
|
||||
router.use(setUserApi); // we need to set the IP before the restriction handler
|
||||
router.use(restrictionHandler);
|
||||
|
||||
router.get('/consent', (_req, res) => {
|
||||
@@ -134,22 +125,10 @@ export default async function initServer() {
|
||||
next();
|
||||
});
|
||||
|
||||
// SESSION
|
||||
router.post('/api/session', loginApi);
|
||||
router.delete('/api/session', logoutApi);
|
||||
|
||||
// USERS
|
||||
router.post('/api/users', signupApi);
|
||||
|
||||
// API KEYS
|
||||
router.get('/api/me/keys', fetchUserKeysApi);
|
||||
router.post('/api/me/keys', createKeyApi);
|
||||
router.delete('/api/me/keys/:keyIdentifier', removeUserKeyApi);
|
||||
router.delete('/api/me/keys', flushUserKeysApi);
|
||||
|
||||
router.use(userRouter);
|
||||
router.use(stashesRouter);
|
||||
router.use(alertsRouter);
|
||||
router.use(authRouter);
|
||||
router.use(scenesRouter);
|
||||
router.use(actorsRouter);
|
||||
router.use(syncRouter);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
fetchUser,
|
||||
fetchUserTemplates,
|
||||
removeTemplate,
|
||||
updateSettings,
|
||||
} from '../users.js';
|
||||
|
||||
async function fetchUserApi(req, res) {
|
||||
@@ -15,14 +14,6 @@ async function fetchUserApi(req, res) {
|
||||
res.send(user);
|
||||
}
|
||||
|
||||
async function updateSettingsApi(req, res) {
|
||||
const updatedSettings = await updateSettings(req.body, req.user);
|
||||
|
||||
req.session.user.settings = updatedSettings;
|
||||
|
||||
res.send(updatedSettings);
|
||||
}
|
||||
|
||||
async function fetchUserTemplatesApi(req, res) {
|
||||
const templates = await fetchUserTemplates(req.user);
|
||||
|
||||
@@ -52,8 +43,6 @@ export const router = Router();
|
||||
router.get('/api/users/:userId', fetchUserApi);
|
||||
router.get('/api/users/:userId/templates', fetchUserTemplatesApi);
|
||||
|
||||
router.patch('/api/me/settings', updateSettingsApi);
|
||||
|
||||
router.post('/api/templates', createTemplateApi);
|
||||
router.delete('/api/templates/:templateId', removeTemplateApi);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user