feat: add community publishing and news syndication
This commit is contained in:
@@ -4,121 +4,219 @@ 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 {
|
||||
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
|
||||
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
|
||||
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
|
||||
${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)) };
|
||||
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)
|
||||
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)
|
||||
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()
|
||||
};
|
||||
}
|
||||
frequency=excluded.frequency,updated_at=excluded.updated_at`)
|
||||
).run(user.id, category.id, frequency, now);
|
||||
}
|
||||
return {
|
||||
oversightPreferencesSaved: true,
|
||||
notificationPreferenceSaveToken: crypto.randomUUID(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user