Files
shd.build/src/routes/account/notifications/+page.server.ts
T
jb dd84dff055
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 50s
Validate and deploy production / deploy (push) Failing after 40s
Validate and deploy staging / deploy (push) Failing after 7s
feat: add community publishing and news syndication
2026-08-01 21:42:09 -04:00

223 lines
8.1 KiB
TypeScript

import { fail, redirect } from '@sveltejs/kit';
import { getSessionUser } from '$lib/server/auth/session';
import { getDatabase } from '$lib/server/database';
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
import { NOTIFICATION_CATEGORIES } from '$lib/server/notifications';
import { getPermissions } from '$lib/server/auth/authorization';
import {
OVERSIGHT_NOTIFICATION_CATEGORIES,
OVERSIGHT_NOTIFICATION_FREQUENCIES,
} from '$lib/server/oversight-notifications';
import {
notificationOrderBy,
parseNotificationSort,
parseNotificationState,
} from '$lib/server/notification-sort';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ cookies, url }) => {
const user = await getSessionUser(cookies);
if (!user) redirect(303, '/sign-in');
const database = await getDatabase();
const state = parseNotificationState(url.searchParams.get('state'));
const sort = parseNotificationSort(url.searchParams.get('sort'));
const { search, pattern } = getRegistrySearch(url);
const conditions = [
'n.user_id=?',
'(n.mandatory=1 OR COALESCE(p.in_app_enabled,1)=1)',
];
const args: unknown[] = [user.id];
if (state === 'unread') conditions.push('n.read_at IS NULL');
if (state === 'read') conditions.push('n.read_at IS NOT NULL');
if (search) {
conditions.push(`(n.title LIKE ? ESCAPE '\\' COLLATE NOCASE
OR n.body LIKE ? ESCAPE '\\' COLLATE NOCASE
OR n.source LIKE ? ESCAPE '\\' COLLATE NOCASE)`);
args.push(pattern, pattern, pattern);
}
const where = `WHERE ${conditions.join(' AND ')}`;
const orderBy = notificationOrderBy(sort);
const total = Number(
(
await (
await database.prepare(
`SELECT COUNT(*) AS total FROM notifications n LEFT JOIN notification_preferences p ON p.user_id=n.user_id AND p.category=n.category ${where}`,
)
).get(...args)
)?.total ?? 0,
);
const pagination = getPagination(url, total);
const notifications = await (
await database.prepare(`SELECT n.* FROM notifications n
LEFT JOIN notification_preferences p ON p.user_id=n.user_id AND p.category=n.category
${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`)
).all(...args, pagination.perPage, pagination.offset);
const preferenceRows = await (
await database.prepare(
'SELECT * FROM notification_preferences WHERE user_id=?',
)
).all(user.id);
const permissions = await getPermissions(user);
const oversightCategories = OVERSIGHT_NOTIFICATION_CATEGORIES.filter(
(category) =>
permissions.includes('*') || permissions.includes(category.permission),
);
const oversightRows = oversightCategories.length
? await (
await database.prepare(
'SELECT * FROM oversight_notification_preferences WHERE user_id=?',
)
).all(user.id)
: [];
const oversightPreferences = oversightCategories.map((category) => ({
...category,
frequency: String(
oversightRows.find((row) => row.oversight_category === category.id)
?.frequency ?? 'immediate',
),
}));
const preferences = NOTIFICATION_CATEGORIES.map((name) => {
const stored = preferenceRows.find((row) => row.category === name);
return {
category: name,
inApp:
name === 'isac'
? true
: stored?.in_app_enabled === undefined
? true
: Boolean(stored.in_app_enabled),
browser: Boolean(stored?.browser_enabled),
emailFrequency: String(stored?.email_frequency ?? 'none'),
};
});
const unread = Number(
(
await (
await database.prepare(`SELECT COUNT(*) AS count FROM notifications n
LEFT JOIN notification_preferences p ON p.user_id=n.user_id AND p.category=n.category
WHERE n.user_id=? AND n.read_at IS NULL AND (n.mandatory=1 OR COALESCE(p.in_app_enabled,1)=1)`)
).get(user.id)
)?.count ?? 0,
);
return {
notifications,
preferences,
oversightPreferences,
pagination,
filters: { state, sort, search },
unread,
hasVerifiedEmail: Boolean(
await (
await database.prepare('SELECT 1 FROM user_emails WHERE user_id=?')
).get(user.id),
),
};
};
export const actions: Actions = {
markRead: async ({ cookies, request }) => {
const user = await getSessionUser(cookies);
if (!user) redirect(303, '/sign-in');
const id = String((await request.formData()).get('id') ?? '');
await (
await (
await getDatabase()
).prepare(
'UPDATE notifications SET read_at=COALESCE(read_at,?) WHERE id=? AND user_id=?',
)
).run(new Date().toISOString(), id, user.id);
return { notificationUpdated: true };
},
markAllRead: async ({ cookies }) => {
const user = await getSessionUser(cookies);
if (!user) redirect(303, '/sign-in');
await (
await (
await getDatabase()
).prepare(
'UPDATE notifications SET read_at=? WHERE user_id=? AND read_at IS NULL',
)
).run(new Date().toISOString(), user.id);
return { notificationsCleared: true };
},
savePreferences: async ({ cookies, request }) => {
const user = await getSessionUser(cookies);
if (!user) redirect(303, '/sign-in');
const form = await request.formData();
const database = await getDatabase();
const now = new Date().toISOString();
const hasEmail = Boolean(
await (
await database.prepare('SELECT 1 FROM user_emails WHERE user_id=?')
).get(user.id),
);
for (const category of NOTIFICATION_CATEGORIES) {
const inApp =
category === 'isac' || form.get(`inApp:${category}`) === 'on';
const browser = form.get(`browser:${category}`) === 'on';
const emailFrequency = String(form.get(`email:${category}`) ?? 'none');
if (!['none', 'immediate', 'daily'].includes(emailFrequency))
return fail(400, {
notificationPreferenceError: 'Choose a valid email frequency.',
});
if (emailFrequency !== 'none' && !hasEmail)
return fail(400, {
notificationPreferenceError:
'Add and verify a magic-link email before enabling email notifications.',
});
await (
await database.prepare(`INSERT INTO notification_preferences(user_id,category,in_app_enabled,browser_enabled,email_frequency,updated_at)
VALUES(?,?,?,?,?,?) ON CONFLICT(user_id,category) DO UPDATE SET
in_app_enabled=excluded.in_app_enabled,browser_enabled=excluded.browser_enabled,email_frequency=excluded.email_frequency,updated_at=excluded.updated_at`)
).run(
user.id,
category,
inApp ? 1 : 0,
browser ? 1 : 0,
emailFrequency,
now,
);
}
return {
notificationPreferencesSaved: true,
notificationPreferenceSaveToken: crypto.randomUUID(),
};
},
saveOversightPreferences: async ({ cookies, request }) => {
const user = await getSessionUser(cookies);
if (!user) redirect(303, '/sign-in');
const permissions = await getPermissions(user);
const available = OVERSIGHT_NOTIFICATION_CATEGORIES.filter(
(category) =>
permissions.includes('*') || permissions.includes(category.permission),
);
if (!available.length)
return fail(403, {
oversightPreferenceError: 'Oversight permission is required.',
});
const form = await request.formData();
const database = await getDatabase();
const now = new Date().toISOString();
for (const category of available) {
const frequency = String(
form.get(`oversight:${category.id}`) ?? 'immediate',
);
if (!OVERSIGHT_NOTIFICATION_FREQUENCIES.includes(frequency as any)) {
return fail(400, {
oversightPreferenceError:
'Choose a valid Oversight notification frequency.',
});
}
await (
await database.prepare(`INSERT INTO oversight_notification_preferences(user_id,oversight_category,frequency,updated_at)
VALUES(?,?,?,?) ON CONFLICT(user_id,oversight_category) DO UPDATE SET
frequency=excluded.frequency,updated_at=excluded.updated_at`)
).run(user.id, category.id, frequency, now);
}
return {
oversightPreferencesSaved: true,
notificationPreferenceSaveToken: crypto.randomUUID(),
};
},
};