Added e-mail alert settings, saveable but not integrated yet.

This commit is contained in:
2026-07-18 06:24:07 +02:00
parent 479b044798
commit 216e7f59ef
18 changed files with 231 additions and 75 deletions

View File

@@ -128,8 +128,6 @@ function toggleFilterCombined() {
<template> <template>
<section class="profile-section"> <section class="profile-section">
<div class="section-header"> <div class="section-header">
<h3 class="heading">Alerts</h3>
<button <button
class="button" class="button"
@click="showAlertDialog = true" @click="showAlertDialog = true"
@@ -377,6 +375,10 @@ function toggleFilterCombined() {
</template> </template>
<style scoped> <style scoped>
.profile-section .section-header {
justify-content: center;
}
.alerts { .alerts {
width: 100%; width: 100%;
margin-bottom: 1rem; margin-bottom: 1rem;

View File

@@ -1,4 +1,6 @@
<script setup> <script setup>
import { useId } from 'vue';
defineProps({ defineProps({
checked: { checked: {
type: Boolean, type: Boolean,
@@ -16,7 +18,7 @@ defineProps({
defineEmits(['change']); defineEmits(['change']);
const uid = String(Math.random()).slice(2); const uid = useId();
</script> </script>
<template> <template>

View File

@@ -23,8 +23,6 @@ async function reloadStashes() {
<template> <template>
<section class="profile-section"> <section class="profile-section">
<div class="section-header"> <div class="section-header">
<h3 class="heading">Stashes</h3>
<button <button
v-if="profile.id === user?.id" v-if="profile.id === user?.id"
class="button" class="button"
@@ -57,6 +55,12 @@ async function reloadStashes() {
</template> </template>
<style scoped> <style scoped>
.profile-section .section-header {
display: flex;
justify-content: center;
margin-bottom: .5rem;
}
.stashes { .stashes {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));

View File

@@ -55,16 +55,7 @@ function copyKey(event) {
<template> <template>
<section class="profile-section"> <section class="profile-section">
<div class="section-header"> <div class="section-header">
<h3 class="heading">API Keys</h3>
<div class="keys-actions"> <div class="keys-actions">
<Icon
v-tooltip="{ content: 'Flush all keys', ariaId: flushTooltipId }"
icon="stack-cancel"
class="keys-flush"
@click="flushKeys"
/>
<button <button
class="button" class="button"
@click="createKey" @click="createKey"
@@ -72,6 +63,13 @@ function copyKey(event) {
<Icon icon="key" /> <Icon icon="key" />
<span class="button-label">New key</span> <span class="button-label">New key</span>
</button> </button>
<Icon
v-tooltip="{ content: 'Flush all keys', ariaId: flushTooltipId }"
icon="stack-cancel"
class="keys-flush"
@click="flushKeys"
/>
</div> </div>
</div> </div>
@@ -154,6 +152,11 @@ function copyKey(event) {
</template> </template>
<style scoped> <style scoped>
.profile-section .section-header {
justify-content: center;
margin-bottom: 1rem;
}
.keys-header { .keys-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -164,6 +167,7 @@ function copyKey(event) {
.keys-actions { .keys-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem;
> .icon { > .icon {
padding: .5rem 1rem; padding: .5rem 1rem;

View File

@@ -6,9 +6,11 @@ import mockupRelease from '#/assets/mockup-release.ts';
import Alerts from '#/components/alerts/alerts.vue'; import Alerts from '#/components/alerts/alerts.vue';
import Revisions from '#/components/edit/revisions.vue'; import Revisions from '#/components/edit/revisions.vue';
import Summaries from '#/components/scenes/summaries.vue'; import Summaries from '#/components/scenes/summaries.vue';
// filters are implemented as generic settings panel in case we want to extend it in the future
import Settings from '#/components/settings/settings.vue';
import Stashes from '#/components/stashes/stashes.vue'; import Stashes from '#/components/stashes/stashes.vue';
import ApiKeys from '#/components/user/api-keys.vue'; import ApiKeys from '#/components/user/api-keys.vue';
import Preferences from '#/components/user/preferences.vue';
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
@@ -18,6 +20,8 @@ const domain = pageContext.routeParams.domain;
const user = pageContext.user; const user = pageContext.user;
const profile = ref(pageContext.pageProps.profile); const profile = ref(pageContext.pageProps.profile);
const showFilters = ref(false);
function scrollHorizontal(event) { function scrollHorizontal(event) {
event.currentTarget.scrollLeft += event.deltaY; // eslint-disable-line no-param-reassign event.currentTarget.scrollLeft += event.deltaY; // eslint-disable-line no-param-reassign
} }
@@ -31,8 +35,14 @@ function scrollHorizontal(event) {
:href="`/user/${profile.username}/`" :href="`/user/${profile.username}/`"
class="user nolink" class="user nolink"
> >
<Icon
v-if="section"
icon="arrow-left3"
class="profile-back"
/>
<img <img
v-if="profile.avatar" v-else-if="profile.avatar"
:src="profile.avatar" :src="profile.avatar"
class="avatar" class="avatar"
> >
@@ -40,7 +50,18 @@ function scrollHorizontal(event) {
<h2 class="username ellipsis">{{ profile.username }}</h2> <h2 class="username ellipsis">{{ profile.username }}</h2>
</a> </a>
<span class="age">{{ formatDistanceStrict(Date.now(), profile.createdAt) }}</span> <strong
v-if="section"
class="profile-crumb"
>
<template v-if="domain">{{ domain }}</template>
{{ section }}
</strong>
<span
v-else
class="age"
>{{ formatDistanceStrict(Date.now(), profile.createdAt) }}</span>
</div> </div>
<nav <nav
@@ -84,6 +105,19 @@ function scrollHorizontal(event) {
class="domain nolink" class="domain nolink"
:class="{ active: section === 'revisions' && domain === 'actors' }" :class="{ active: section === 'revisions' && domain === 'actors' }"
><Icon icon="profile" />Actor Revisions</a> ><Icon icon="profile" />Actor Revisions</a>
<span
:href="`/user/${profile.username}/preferences`"
class="domain nolink"
:class="{ active: section === 'filters' }"
@click="showFilters = true"
><Icon icon="filter" />Filters</span>
<a
:href="`/user/${profile.username}/preferences`"
class="domain nolink"
:class="{ active: section === 'preferences' }"
><Icon icon="equalizer" />Preferences</a>
</nav> </nav>
<Stashes v-if="section === 'stashes'" /> <Stashes v-if="section === 'stashes'" />
@@ -97,15 +131,23 @@ function scrollHorizontal(event) {
<ApiKeys <ApiKeys
v-if="section === 'api'" v-if="section === 'api'"
/> />
<Preferences
v-if="section === 'preferences'"
/>
</div> </div>
<div <div
v-if="section === 'revisions' && profile.id === user?.id" v-if="section === 'revisions' && profile.id === user?.id"
class="profile-section revisions" class="profile-section revisions"
> >
<h3 class="section-header heading">{{ domain.slice(0, -1) }} Revisions</h3>
<Revisions context="user" /> <Revisions context="user" />
</div> </div>
<Settings
v-if="showFilters"
@close="showFilters = false"
/>
</div> </div>
</template> </template>
@@ -169,6 +211,10 @@ function scrollHorizontal(event) {
.user { .user {
display: flex; display: flex;
overflow: hidden; overflow: hidden;
&:hover .profile-back {
fill: var(--primary);
}
} }
.username { .username {
@@ -196,6 +242,16 @@ function scrollHorizontal(event) {
margin-right: 1rem; margin-right: 1rem;
} }
.profile-back {
padding: .25rem 1.25rem .25rem .25rem;
fill: var(--highlight);
transform: translateY(-1px);
}
.profile-crumb {
text-transform: capitalize;
}
.domains { .domains {
width: 100%; width: 100%;
display: grid; display: grid;
@@ -228,6 +284,7 @@ function scrollHorizontal(event) {
&:hover { &:hover {
background: var(--primary); background: var(--primary);
color: var(--text-light); color: var(--text-light);
cursor: pointer;
.icon { .icon {
fill: var(--text-light); fill: var(--text-light);
@@ -249,6 +306,7 @@ function scrollHorizontal(event) {
@media(--compact) { @media(--compact) {
.domains { .domains {
padding: .5rem 1rem; padding: .5rem 1rem;
grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
} }
.profile { .profile {
@@ -260,6 +318,12 @@ function scrollHorizontal(event) {
} }
} }
@media(--small) {
.domains {
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
}
}
@media(--small-20) { @media(--small-20) {
.profile-header { .profile-header {
padding: .5rem; padding: .5rem;

View File

@@ -73,7 +73,7 @@ export async function login(credentials, userIp) {
} }
// fetched the raw user for password verification, don't return directly to user // fetched the raw user for password verification, don't return directly to user
return curateUser(user); return curateUser(user, user);
} }
export async function signup(credentials, userIp) { export async function signup(credentials, userIp) {

View File

@@ -14,13 +14,18 @@ function curateTemplate(template) {
}; };
} }
export function curateUser(user, _assets = {}, reqUser) { export function curateUser(user, reqUser) {
if (!user) { if (!user) {
return null; return null;
} }
// const curatedStashes = assets.stashes?.filter(Boolean).map((stash) => curateStash(stash)) || []; // const curatedStashes = assets.stashes?.filter(Boolean).map((stash) => curateStash(stash)) || [];
const privateUser = {
email: user.email,
settings: user.settings,
};
const curatedUser = { const curatedUser = {
id: user.id, id: user.id,
username: user.username, username: user.username,
@@ -30,11 +35,7 @@ export function curateUser(user, _assets = {}, reqUser) {
role: user.role, role: user.role,
abilities: [...user.role_abilities || [], ...user.abilities || []], abilities: [...user.role_abilities || [], ...user.abilities || []],
createdAt: user.created_at, createdAt: user.created_at,
...(reqUser?.id === user.id ...(reqUser?.id === user.id ? privateUser : null),
? {
email: user.email,
}
: null),
}; };
return curatedUser; return curatedUser;
@@ -67,12 +68,10 @@ export async function fetchUser(userId, options = {}, reqUser) {
} }
if (options.raw) { if (options.raw) {
// return { user, stashes, templates };
return { user }; return { user };
} }
// return curateUser(user, { stashes, templates }); return curateUser(user, reqUser);
return curateUser(user, {}, reqUser);
} }
export async function fetchUserTemplates(reqUser) { export async function fetchUserTemplates(reqUser) {
@@ -137,3 +136,35 @@ export async function createBan(ban, reqUser) {
await knex('bans').insert(curatedBan); await knex('bans').insert(curatedBan);
} }
const validSettingKeys = [
'alertEmailEnabled',
'alertEmailFrequency',
];
export async function updateSettings(settings, reqUser) {
if (!reqUser) {
throw new HttpError('You need to be authenticated to change your settings', 401);
}
console.log('SETTINGS', settings);
const invalidKeys = Object.keys(settings).filter((key) => !validSettingKeys.includes(key));
if (invalidKeys.length > 0) {
throw new HttpError(`Invalid settings: ${invalidKeys.join()}`, 400);
}
if (settings.alertEmailFrequency && !['instant', 'daily', 'weekly', 'monthly'].includes(settings.alertEmailFrequency)) {
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');
console.log('UPDATED SETTINGS', updatedSettings);
return updatedSettings;
}

View File

@@ -1,4 +1,3 @@
import { stringify } from '@brillout/json-serializer/stringify';
import { Router } from 'express'; import { Router } from 'express';
import omit from 'object.omit'; import omit from 'object.omit';
@@ -45,12 +44,12 @@ export async function fetchActorsApi(req, res) {
order: req.query.order?.split('.') || ['likes', 'desc'], order: req.query.order?.split('.') || ['likes', 'desc'],
}, req.user); }, req.user);
res.send(stringify({ res.send({
actors, actors,
countries, countries,
limit, limit,
total, total,
})); });
} }
export const actorsSchema = ` export const actorsSchema = `
@@ -189,7 +188,7 @@ export async function fetchActorsByIdGraphql(query, _req, _info) {
export async function createActorApi(req, res) { export async function createActorApi(req, res) {
const actor = await createActor(req.body.actor, omit(req.body, ['actor']), req.user); const actor = await createActor(req.body.actor, omit(req.body, ['actor']), req.user);
res.send(stringify({ actor })); res.send({ actor });
} }
export async function mergeActorsApi(req, res) { export async function mergeActorsApi(req, res) {
@@ -200,13 +199,13 @@ export async function mergeActorsApi(req, res) {
req.user, req.user,
); );
res.send(stringify(result)); res.send(result);
} }
async function fetchActorRevisionsApi(req, res) { async function fetchActorRevisionsApi(req, res) {
const revisions = await fetchActorRevisions(Number(req.params.revisionId) || null, req.query, req.user); const revisions = await fetchActorRevisions(Number(req.params.revisionId) || null, req.query, req.user);
res.send(stringify(revisions)); res.send(revisions);
} }
async function createActorRevisionApi(req, res) { async function createActorRevisionApi(req, res) {

View File

@@ -1,4 +1,3 @@
import { stringify } from '@brillout/json-serializer/stringify';
import { Router } from 'express'; import { Router } from 'express';
import { import {
@@ -147,7 +146,7 @@ export const alertsSchema = `
export async function fetchAlertsApi(req, res) { export async function fetchAlertsApi(req, res) {
const alerts = await fetchAlerts(req.user); const alerts = await fetchAlerts(req.user);
res.send(stringify(alerts)); res.send(alerts);
} }
export async function fetchAlertsGraphql(query, req) { export async function fetchAlertsGraphql(query, req) {
@@ -159,7 +158,7 @@ export async function fetchAlertsGraphql(query, req) {
export async function createAlertApi(req, res) { export async function createAlertApi(req, res) {
const alert = await createAlert(req.body, req.user); const alert = await createAlert(req.body, req.user);
res.send(stringify(alert)); res.send(alert);
} }
export async function createAlertGraphql(query, req) { export async function createAlertGraphql(query, req) {
@@ -185,7 +184,7 @@ export async function fetchNotificationsApi(req, res) {
limit: req.query.limit || 10, limit: req.query.limit || 10,
}); });
res.send(stringify(notifications)); res.send(notifications);
} }
export async function fetchNotificationsGraphql(query, req) { export async function fetchNotificationsGraphql(query, req) {

View File

@@ -1,4 +1,4 @@
import { stringify } from '@brillout/json-serializer/stringify'; import crypto from 'node:crypto';
import IPCIDR from 'ip-cidr'; import IPCIDR from 'ip-cidr';
import argv from '../argv.js'; import argv from '../argv.js';
@@ -13,6 +13,46 @@ import {
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
const runtimeId = crypto.randomUUID();
const sessionStaleThreshold = 5 * 60 * 1000;
// spread into a new object rather than mutating the user we're about to hand back to the
// client (loginApi/signupApi send this straight to the browser) — runtimeId/refreshedAt are
// session bookkeeping only, they shouldn't leak into the API response
function toSession(user) {
return {
...user,
runtimeId,
refreshedAt: Date.now(),
};
}
function isSessionStale(sessionUser) {
return sessionUser.runtimeId !== runtimeId || Date.now() - sessionUser.refreshedAt > sessionStaleThreshold;
}
// concurrent requests for the same user (e.g. the burst of asset/API requests a single page
// load fires) share one in-flight refresh instead of each independently hitting the DB
const pendingSessionRefreshes = new Map();
async function refreshSessionUser(sessionUser) {
const pending = pendingSessionRefreshes.get(sessionUser.id);
if (pending) {
return pending;
}
const refresh = fetchUser(sessionUser.id, {}, sessionUser)
.then((user) => toSession(user))
.finally(() => pendingSessionRefreshes.delete(sessionUser.id));
pendingSessionRefreshes.set(sessionUser.id, refresh);
return refresh;
}
function getIp(req) { function getIp(req) {
if (argv.ip) { if (argv.ip) {
return argv.ip; return argv.ip;
@@ -47,6 +87,10 @@ export async function setUserApi(req, res, next) {
req.userIp = ip; req.userIp = ip;
if (req.session.user) { if (req.session.user) {
if (isSessionStale(req.session.user)) {
req.session.user = await refreshSessionUser(req.session.user);
}
req.user = req.session.user; req.user = req.session.user;
req.user.ip = ip; req.user.ip = ip;
} }
@@ -55,7 +99,7 @@ export async function setUserApi(req, res, next) {
} }
export async function updateSessionUser(req) { export async function updateSessionUser(req) {
const user = await fetchUser(req.session.user.id, {}, req.session.user); const user = toSession(await fetchUser(req.session.user.id, {}, req.session.user));
req.session.user = user; req.session.user = user;
@@ -66,8 +110,8 @@ export async function updateSessionUser(req) {
export async function loginApi(req, res) { export async function loginApi(req, res) {
const user = await login(req.body, req.userIp); const user = await login(req.body, req.userIp);
req.session.user = user; req.session.user = toSession(user);
res.send(stringify(user)); res.send(user);
} }
export async function logoutApi(req, res) { export async function logoutApi(req, res) {
@@ -83,20 +127,20 @@ export async function logoutApi(req, res) {
export async function signupApi(req, res) { export async function signupApi(req, res) {
const user = await signup(req.body, req.userIp); const user = await signup(req.body, req.userIp);
req.session.user = user; req.session.user = toSession(user);
res.send(stringify(user)); res.send(user);
} }
export async function fetchUserKeysApi(req, res) { export async function fetchUserKeysApi(req, res) {
const keys = await fetchUserKeys(req.user); const keys = await fetchUserKeys(req.user);
res.send(stringify(keys)); res.send(keys);
} }
export async function createKeyApi(req, res) { export async function createKeyApi(req, res) {
const key = await createKey(req.user); const key = await createKey(req.user);
res.send(stringify(key)); res.send(key);
} }
export async function removeUserKeyApi(req, res) { export async function removeUserKeyApi(req, res) {

View File

@@ -1,4 +1,3 @@
import { stringify } from '@brillout/json-serializer/stringify';
import { parseResolveInfo } from 'graphql-parse-resolve-info'; import { parseResolveInfo } from 'graphql-parse-resolve-info';
import { getIdsBySlug } from '../cache.js'; import { getIdsBySlug } from '../cache.js';
@@ -11,7 +10,7 @@ import {
export async function fetchEntitiesApi(req, res) { export async function fetchEntitiesApi(req, res) {
const entities = await fetchEntities(req.query); const entities = await fetchEntities(req.query);
res.send(stringify(entities)); res.send(entities);
} }
export const entitiesSchema = ` export const entitiesSchema = `

View File

@@ -26,6 +26,7 @@ export default async function mainHandler(req, res, next) {
id: req.user.id, id: req.user.id,
username: req.user.username, username: req.user.username,
email: req.user.email, email: req.user.email,
settings: req.user.settings,
role: req.user.role, role: req.user.role,
abilities: req.user.abilities, abilities: req.user.abilities,
avatar: req.user.avatar, avatar: req.user.avatar,

View File

@@ -1,5 +1,3 @@
import { stringify } from '@brillout/json-serializer/stringify';
import promiseProps from '../../utils/promise-props.js'; import promiseProps from '../../utils/promise-props.js';
import slugify from '../../utils/slugify.js'; import slugify from '../../utils/slugify.js';
import { getIdsBySlug } from '../cache.js'; import { getIdsBySlug } from '../cache.js';
@@ -35,7 +33,7 @@ export async function fetchMoviesApi(req, res) {
limit: Number(req.query.limit) || 30, limit: Number(req.query.limit) || 30,
}, req.user, { restriction: req.restriction }); }, req.user, { restriction: req.restriction });
res.send(stringify({ res.send({
movies, movies,
aggYears, aggYears,
aggActors, aggActors,
@@ -43,7 +41,7 @@ export async function fetchMoviesApi(req, res) {
aggChannels, aggChannels,
limit, limit,
total, total,
})); });
} }
export async function fetchMovieApi(req, res) { export async function fetchMovieApi(req, res) {
@@ -53,7 +51,7 @@ export async function fetchMovieApi(req, res) {
throw new HttpError(`No movie with ID ${req.params.movieId} found`, 404); throw new HttpError(`No movie with ID ${req.params.movieId} found`, 404);
} }
res.send(stringify(movie)); res.send(movie);
} }
export const moviesSchema = ` export const moviesSchema = `

View File

@@ -1,4 +1,3 @@
import { stringify } from '@brillout/json-serializer/stringify';
import { Router } from 'express'; import { Router } from 'express';
import promiseProps from '../../utils/promise-props.js'; import promiseProps from '../../utils/promise-props.js';
@@ -74,7 +73,7 @@ async function fetchScenesApi(req, res) {
restriction: req.restriction, restriction: req.restriction,
}); });
res.send(stringify({ res.send({
scenes, scenes,
aggYears, aggYears,
aggActors, aggActors,
@@ -83,7 +82,7 @@ async function fetchScenesApi(req, res) {
aggChannels, aggChannels,
limit, limit,
total, total,
})); });
} }
export const scenesSchema = ` export const scenesSchema = `
@@ -261,7 +260,7 @@ async function fetchSceneApi(req, res) {
throw new HttpError(`No scene with ID ${req.params.sceneId} found`, 404); throw new HttpError(`No scene with ID ${req.params.sceneId} found`, 404);
} }
res.send(stringify(scene)); res.send(scene);
} }
export async function fetchScenesByIdGraphql(query, req) { export async function fetchScenesByIdGraphql(query, req) {
@@ -281,7 +280,7 @@ export async function fetchScenesByIdGraphql(query, req) {
async function fetchSceneRevisionsApi(req, res) { async function fetchSceneRevisionsApi(req, res) {
const revisions = await fetchSceneRevisions(Number(req.params.revisionId) || null, req.query, req.user); const revisions = await fetchSceneRevisions(Number(req.params.revisionId) || null, req.query, req.user);
res.send(stringify(revisions)); res.send(revisions);
} }
async function createSceneRevisionApi(req, res) { async function createSceneRevisionApi(req, res) {

View File

@@ -37,6 +37,7 @@ import { fetchMoviesApi } from './movies.js';
import initRestrictionHandler from './restrictions.js'; import initRestrictionHandler from './restrictions.js';
import { scenesRouter } from './scenes.js'; import { scenesRouter } from './scenes.js';
import serializeApiResponses from './serialize.js';
import { router as stashesRouter } from './stashes.js'; import { router as stashesRouter } from './stashes.js';
import { syncRouter } from './sync.js'; import { syncRouter } from './sync.js';
@@ -123,6 +124,8 @@ export default async function initServer() {
res.sendFile(path.join(import.meta.dirname, '../../assets/consent.html')); res.sendFile(path.join(import.meta.dirname, '../../assets/consent.html'));
}); });
router.use('/api/{*splat}', serializeApiResponses);
router.use('/api/{*splat}', async (req, _res, next) => { router.use('/api/{*splat}', async (req, _res, next) => {
if (req.headers['api-user']) { if (req.headers['api-user']) {
req.user = await verifyKey(req.headers['api-user'], req.headers['api-key'], req); req.user = await verifyKey(req.headers['api-user'], req.headers['api-key'], req);

View File

@@ -1,4 +1,3 @@
import { stringify } from '@brillout/json-serializer/stringify';
import { Router } from 'express'; import { Router } from 'express';
import { import {
@@ -101,7 +100,7 @@ export const stashesSchema = `
export async function fetchUserStashesApi(req, res) { export async function fetchUserStashesApi(req, res) {
const stashes = await fetchUserStashes(req.user.id, req.user); const stashes = await fetchUserStashes(req.user.id, req.user);
res.send(stringify(stashes)); res.send(stashes);
} }
export async function fetchUserStashesGraphql(query, req) { export async function fetchUserStashesGraphql(query, req) {
@@ -119,7 +118,7 @@ export async function fetchStashGraphql(query, req) {
export async function createStashApi(req, res) { export async function createStashApi(req, res) {
const stash = await createStash(req.body, req.user); const stash = await createStash(req.body, req.user);
res.send(stringify(stash)); res.send(stash);
} }
export async function createStashGraphql(query, req) { export async function createStashGraphql(query, req) {
@@ -131,7 +130,7 @@ export async function createStashGraphql(query, req) {
export async function updateStashApi(req, res) { export async function updateStashApi(req, res) {
const stash = await updateStash(Number(req.params.stashId), req.body, req.user); const stash = await updateStash(Number(req.params.stashId), req.body, req.user);
res.send(stringify(stash)); res.send(stash);
} }
export async function updateStashGraphql(query, req) { export async function updateStashGraphql(query, req) {
@@ -155,7 +154,7 @@ export async function removeStashGraphql(query, req) {
export async function stashActorApi(req, res) { export async function stashActorApi(req, res) {
const stashed = await stashActor(req.body.actorId, Number(req.params.stashId), req.user); const stashed = await stashActor(req.body.actorId, Number(req.params.stashId), req.user);
res.send(stringify(stashed)); res.send(stashed);
} }
export async function stashActorGraphql(query, req) { export async function stashActorGraphql(query, req) {
@@ -167,7 +166,7 @@ export async function stashActorGraphql(query, req) {
export async function unstashActorApi(req, res) { export async function unstashActorApi(req, res) {
const unstashed = await unstashActor(Number(req.params.actorId), Number(req.params.stashId), req.user); const unstashed = await unstashActor(Number(req.params.actorId), Number(req.params.stashId), req.user);
res.send(stringify(unstashed)); res.send(unstashed);
} }
export async function unstashActorGraphql(query, req) { export async function unstashActorGraphql(query, req) {
@@ -179,7 +178,7 @@ export async function unstashActorGraphql(query, req) {
export async function stashSceneApi(req, res) { export async function stashSceneApi(req, res) {
const stashed = await stashScene(req.body.sceneId, Number(req.params.stashId), req.user); const stashed = await stashScene(req.body.sceneId, Number(req.params.stashId), req.user);
res.send(stringify(stashed)); res.send(stashed);
} }
export async function stashSceneGraphql(query, req) { export async function stashSceneGraphql(query, req) {
@@ -191,7 +190,7 @@ export async function stashSceneGraphql(query, req) {
export async function unstashSceneApi(req, res) { export async function unstashSceneApi(req, res) {
const unstashed = await unstashScene(Number(req.params.sceneId), Number(req.params.stashId), req.user); const unstashed = await unstashScene(Number(req.params.sceneId), Number(req.params.stashId), req.user);
res.send(stringify(unstashed)); res.send(unstashed);
} }
export async function unstashSceneGraphql(query, req) { export async function unstashSceneGraphql(query, req) {
@@ -203,7 +202,7 @@ export async function unstashSceneGraphql(query, req) {
export async function stashMovieApi(req, res) { export async function stashMovieApi(req, res) {
const stashed = await stashMovie(req.body.movieId, Number(req.params.stashId), req.user); const stashed = await stashMovie(req.body.movieId, Number(req.params.stashId), req.user);
res.send(stringify(stashed)); res.send(stashed);
} }
export async function stashMovieGraphql(query, req) { export async function stashMovieGraphql(query, req) {
@@ -215,7 +214,7 @@ export async function stashMovieGraphql(query, req) {
export async function unstashMovieApi(req, res) { export async function unstashMovieApi(req, res) {
const unstashed = await unstashMovie(Number(req.params.movieId), Number(req.params.stashId), req.user); const unstashed = await unstashMovie(Number(req.params.movieId), Number(req.params.stashId), req.user);
res.send(stringify(unstashed)); res.send(unstashed);
} }
export async function unstashMovieGraphql(query, req) { export async function unstashMovieGraphql(query, req) {

View File

@@ -1,5 +1,3 @@
import { stringify } from '@brillout/json-serializer/stringify';
import { fetchTags } from '../tags.js'; import { fetchTags } from '../tags.js';
export async function fetchTagsApi(req, res) { export async function fetchTagsApi(req, res) {
@@ -9,5 +7,5 @@ export async function fetchTagsApi(req, res) {
restriction: req.restriction, restriction: req.restriction,
}); });
res.send(stringify(tags)); res.send(tags);
} }

View File

@@ -1,4 +1,3 @@
import { stringify } from '@brillout/json-serializer/stringify';
import { Router } from 'express'; import { Router } from 'express';
import { import {
@@ -7,24 +6,33 @@ import {
fetchUser, fetchUser,
fetchUserTemplates, fetchUserTemplates,
removeTemplate, removeTemplate,
updateSettings,
} from '../users.js'; } from '../users.js';
async function fetchUserApi(req, res) { async function fetchUserApi(req, res) {
const user = await fetchUser(req.params.userId, {}, req.user); const user = await fetchUser(req.params.userId, {}, req.user);
res.send(stringify(user)); 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) { async function fetchUserTemplatesApi(req, res) {
const templates = await fetchUserTemplates(req.user); const templates = await fetchUserTemplates(req.user);
res.send(stringify(templates)); res.send(templates);
} }
async function createTemplateApi(req, res) { async function createTemplateApi(req, res) {
const template = await createTemplate(req.body, req.user); const template = await createTemplate(req.body, req.user);
res.send(stringify(template)); res.send(template);
} }
async function removeTemplateApi(req, res) { async function removeTemplateApi(req, res) {
@@ -44,6 +52,8 @@ export const router = Router();
router.get('/api/users/:userId', fetchUserApi); router.get('/api/users/:userId', fetchUserApi);
router.get('/api/users/:userId/templates', fetchUserTemplatesApi); router.get('/api/users/:userId/templates', fetchUserTemplatesApi);
router.patch('/api/me/settings', updateSettingsApi);
router.post('/api/templates', createTemplateApi); router.post('/api/templates', createTemplateApi);
router.delete('/api/templates/:templateId', removeTemplateApi); router.delete('/api/templates/:templateId', removeTemplateApi);