(refactor) migrate /api/names/** from express to h3

This commit is contained in:
Valentyne Stigloher 2025-03-20 14:05:57 +01:00
parent 317758db01
commit 0299dbec0e
11 changed files with 203 additions and 173 deletions

View File

@ -6,7 +6,7 @@ import useConfig from '~/composables/useConfig.ts';
import useDialogue from '~/composables/useDialogue.ts';
import useHash from '~/composables/useHash.ts';
import useSimpleHead from '~/composables/useSimpleHead.ts';
import { Name, type NameRaw } from '~/src/classes.ts';
import { Name } from '~/src/classes.ts';
import { buildDict } from '~/src/helpers.ts';
definePageMeta({
@ -21,7 +21,7 @@ useSimpleHead({
const { handleHash, setHash } = useHash();
const namesAsyncData = useAsyncData(async () => {
const namesRaw = await $fetch<NameRaw[]>('/api/names');
const namesRaw = await $fetch('/api/names');
return buildDict(function* () {
const sorted = namesRaw.sort((a, b) => {

View File

@ -0,0 +1,19 @@
import { auditLog } from '~/server/audit.ts';
import { getLocale } from '~/server/data.ts';
import { approveNameEntry } from '~/server/names.ts';
export default defineEventHandler(async (event) => {
const { user, isGranted } = await useAuthentication(event);
if (!isGranted('names')) {
throw createError({
status: 401,
statusMessage: 'Unauthorised',
});
}
const id = getRouterParam(event, 'id')!;
const db = useDatabase();
await approveNameEntry(db, id, getLocale(event));
await auditLog({ user }, 'names/approved', { id });
});

View File

@ -0,0 +1,26 @@
import SQL from 'sql-template-strings';
import { auditLog } from '~/server/audit.ts';
import { getLocale } from '~/server/data.ts';
export default defineEventHandler(async (event) => {
const { user, isGranted } = await useAuthentication(event);
if (!isGranted('names')) {
throw createError({
status: 401,
statusMessage: 'Unauthorised',
});
}
const id = getRouterParam(event, 'id');
const db = useDatabase();
await db.get(SQL`
UPDATE names
SET approved = 0
WHERE id = ${id}
`);
await invalidateCache('names', getLocale(event));
await auditLog({ user }, 'names/hidden', { id });
});

View File

@ -0,0 +1,8 @@
import { getLocale } from '~/server/data.ts';
import { getNameEntries } from '~/server/names.ts';
export default defineEventHandler(async (event) => {
const { isGranted } = await useAuthentication(event);
const db = useDatabase();
return await getNameEntries(db, isGranted, getLocale(event));
});

View File

@ -0,0 +1,26 @@
import SQL from 'sql-template-strings';
import { auditLog } from '~/server/audit.ts';
import { getLocale } from '~/server/data.ts';
export default defineEventHandler(async (event) => {
const { user, isGranted } = await useAuthentication(event);
if (!isGranted('names')) {
throw createError({
status: 401,
statusMessage: 'Unauthorised',
});
}
const id = getRouterParam(event, 'id');
const db = useDatabase();
await db.get(SQL`
UPDATE names
SET deleted=1
WHERE id = ${id}
`);
await invalidateCache('names', getLocale(event));
await auditLog({ user }, 'names/removed', { id });
});

View File

@ -0,0 +1,44 @@
import SQL from 'sql-template-strings';
import { ulid } from 'ulid';
import { auditLog } from '~/server/audit.ts';
import { getLocale } from '~/server/data.ts';
import { approveNameEntry } from '~/server/names.ts';
import { isAllowedToPost } from '~/server/user.ts';
export default defineEventHandler(async (event) => {
const { user, isGranted } = await useAuthentication(event);
const db = useDatabase();
if (!user || !await isAllowedToPost(db, user)) {
throw createError({
status: 401,
statusMessage: 'Unauthorised',
});
}
const locale = getLocale(event);
const body = await readBody(event);
const id = ulid();
await db.get(SQL`
INSERT INTO names (id, name, locale, origin, meaning, usage, legally, pros, cons, notablePeople, links, namedays, namedaysComment, deleted, approved, base_id, author_id)
VALUES (
${id},
${body.name}, ${locale},
${body.origin || null}, ${body.meaning || null}, ${body.usage || null}, ${body.legally || null},
${body.pros.length ? body.pros.join('|') : null}, ${body.cons.length ? body.cons.join('|') : null},
${body.notablePeople.length ? body.notablePeople.join('|') : null}, ${body.links.length ? body.links.join('|') : null},
${body.namedays.length ? body.namedays.join('|') : null}, ${body.namedaysComment || null},
0, 0, ${body.base}, ${user.id}
)
`);
await auditLog({ user }, 'names/submitted', body);
if (isGranted('names')) {
await approveNameEntry(db, id, locale);
await auditLog({ user }, 'names/approved', { id });
}
setResponseStatus(event, 201, 'Created');
});

View File

@ -1,151 +0,0 @@
import { Router } from 'express';
import type { Request } from 'express';
import { getH3Event } from 'h3-express';
import SQL from 'sql-template-strings';
import { ulid } from 'ulid';
import { handleErrorAsync } from '../../src/helpers.ts';
import { auditLog } from '../audit.ts';
import type { Database } from '../db.ts';
import { getLocale } from '~/server/data.ts';
interface NameRow {
id: string;
name: string;
locale: string;
origin: string | null;
meaning: string | null;
usage: string | null;
legally: string | null;
pros: string | null;
cons: string | null;
notablePeople: string | null;
links: string | null;
namedays: string | null;
namedaysComment: string | null;
deleted: number;
approved: number;
base_id: string | null;
author_id: string | null;
}
const approve = async (db: Database, id: string, locale: string) => {
const { base_id } = (await db.get<Pick<NameRow, 'base_id'>>(SQL`SELECT base_id FROM names WHERE id=${id}`))!;
if (base_id) {
await db.get(SQL`
UPDATE names
SET deleted=1
WHERE id = ${base_id}
`);
}
await db.get(SQL`
UPDATE names
SET approved = 1, base_id = NULL
WHERE id = ${id}
`);
await invalidateCache('names', locale);
};
const router = Router();
const getNames = defineCachedFunction(async (db: Database, isGranted: Request['isGranted'], locale: string) => {
return await db.all(SQL`
SELECT n.*, u.username AS author FROM names n
LEFT JOIN users u ON n.author_id = u.id
WHERE n.locale = ${locale}
AND n.deleted = 0
AND n.approved >= ${isGranted('names') ? 0 : 1}
ORDER BY n.approved, n.name
`);
}, {
name: 'names',
getKey: (db, isGranted, locale) => locale,
shouldBypassCache: (db, isGranted) => isGranted('names'),
maxAge: 24 * 60 * 60,
});
router.get('/names', handleErrorAsync(async (req, res) => {
const locale = getLocale(getH3Event(req));
return res.json(await getNames(req.db, req.isGranted, locale));
}));
router.post('/names/submit', handleErrorAsync(async (req, res) => {
if (!req.user || !await req.isUserAllowedToPost()) {
return res.status(401).json({ error: 'Unauthorised' });
}
const locale = getLocale(getH3Event(req));
const id = ulid();
await req.db.get(SQL`
INSERT INTO names (id, name, locale, origin, meaning, usage, legally, pros, cons, notablePeople, links, namedays, namedaysComment, deleted, approved, base_id, author_id)
VALUES (
${id},
${req.body.name}, ${locale},
${req.body.origin || null}, ${req.body.meaning || null}, ${req.body.usage || null}, ${req.body.legally || null},
${req.body.pros.length ? req.body.pros.join('|') : null}, ${req.body.cons.length ? req.body.cons.join('|') : null},
${req.body.notablePeople.length ? req.body.notablePeople.join('|') : null}, ${req.body.links.length ? req.body.links.join('|') : null},
${req.body.namedays.length ? req.body.namedays.join('|') : null}, ${req.body.namedaysComment || null},
0, 0, ${req.body.base}, ${req.user ? req.user.id : null}
)
`);
await auditLog(req, 'names/submitted', { ...req.body });
if (req.isGranted('names')) {
await approve(req.db, id, locale);
await auditLog(req, 'names/approved', { id });
}
return res.json('ok');
}));
router.post('/names/hide/:id', handleErrorAsync(async (req, res) => {
if (!req.isGranted('names')) {
return res.status(401).json({ error: 'Unauthorised' });
}
await req.db.get(SQL`
UPDATE names
SET approved = 0
WHERE id = ${req.params.id}
`);
await invalidateCache('names', getLocale(getH3Event(req)));
await auditLog(req, 'names/hidden', { id: req.params.id });
return res.json('ok');
}));
router.post('/names/approve/:id', handleErrorAsync(async (req, res) => {
if (!req.isGranted('names')) {
return res.status(401).json({ error: 'Unauthorised' });
}
await approve(req.db, req.params.id, getLocale(getH3Event(req)));
await auditLog(req, 'names/approved', { id: req.params.id });
return res.json('ok');
}));
router.post('/names/remove/:id', handleErrorAsync(async (req, res) => {
if (!req.isGranted('names')) {
return res.status(401).json({ error: 'Unauthorised' });
}
await req.db.get(SQL`
UPDATE names
SET deleted=1
WHERE id = ${req.params.id}
`);
await invalidateCache('names', getLocale(getH3Event(req)));
await auditLog(req, 'names/removed', { id: req.params.id });
return res.json('ok');
}));
export default router;

1
server/global.d.ts vendored
View File

@ -13,7 +13,6 @@ declare global {
isGranted: (area?: string, locale?: string) => boolean;
locales: Record<string, LocaleDescription>;
db: Database;
isUserAllowedToPost: () => Promise<boolean>;
}
export interface Response {

View File

@ -22,7 +22,6 @@ import discord from './express/discord.ts';
import grantOverridesRoute from './express/grantOverrides.ts';
import imagesRoute from './express/images.ts';
import mfaRoute from './express/mfa.ts';
import namesRoute from './express/names.ts';
import profileRoute from './express/profile.ts';
import pronounceRoute from './express/pronounce.ts';
import sentryRoute from './express/sentry.ts';
@ -33,7 +32,6 @@ import { config } from './social.ts';
import { closeAuditLogConnection } from '~/server/audit.ts';
import { getLocale } from '~/server/data.ts';
import { isAllowedToPost } from '~/server/user.ts';
class StorageStore extends session.Store {
get(sid: string, callback: (err: unknown, session?: (session.SessionData | null)) => void): void {
@ -133,7 +131,6 @@ router.use(async function (req, res, next) {
req.isGranted = authentication.isGranted;
req.locales = buildLocaleList(locale, locale === '_');
req.db = new LazyDatabase();
req.isUserAllowedToPost = (): Promise<boolean> => isAllowedToPost(req.db, req.user);
res.on('finish', async () => {
await req.db.close();
await closeAuditLogConnection();
@ -159,7 +156,6 @@ router.use(adminRoute);
router.use(mfaRoute);
router.use(pronounceRoute);
router.use(censusRoute);
router.use(namesRoute);
router.use(imagesRoute);
router.use(calendarRoute);
router.use(translationsRoute);

63
server/names.ts Normal file
View File

@ -0,0 +1,63 @@
import SQL from 'sql-template-strings';
import type { Database } from '~/server/db.ts';
import type { User } from '~/src/user.ts';
interface NameRow {
id: string;
name: string;
locale: string;
origin: string | null;
meaning: string | null;
usage: string | null;
legally: string | null;
pros: string | null;
cons: string | null;
notablePeople: string | null;
links: string | null;
namedays: string | null;
namedaysComment: string | null;
deleted: boolean;
approved: boolean;
base_id: string | null;
author_id: string | null;
}
type NameRowWithAuthor = NameRow & { author: User['username'] };
export const getNameEntries = defineCachedFunction(async (
db: Database,
isGranted: IsGrantedFn,
locale: string,
) => {
return await db.all<NameRowWithAuthor>(SQL`
SELECT n.*, u.username AS author FROM names n
LEFT JOIN users u ON n.author_id = u.id
WHERE n.locale = ${locale}
AND n.deleted = 0
AND n.approved >= ${isGranted('names') ? 0 : 1}
ORDER BY n.approved, n.name
`);
}, {
name: 'names',
getKey: (db, isGranted, locale) => locale,
shouldBypassCache: (db, isGranted) => isGranted('names'),
maxAge: 24 * 60 * 60,
});
export const approveNameEntry = async (db: Database, id: string, locale: string) => {
const { base_id } = (await db.get<Pick<NameRow, 'base_id'>>(SQL`SELECT base_id FROM names WHERE id=${id}`))!;
if (base_id) {
await db.get(SQL`
UPDATE names
SET deleted=1
WHERE id = ${base_id}
`);
}
await db.get(SQL`
UPDATE names
SET approved = 1, base_id = NULL
WHERE id = ${id}
`);
await invalidateCache('names', locale);
};

View File

@ -1317,16 +1317,16 @@ export class TermsEntry implements Entry {
export interface NameRaw {
id: string;
name: string;
origin: string;
meaning: string;
usage: string;
legally: string;
pros: string;
cons: string;
notablePeople: string;
links: string;
namedays: string;
namedaysComment: string;
origin: string | null;
meaning: string | null;
usage: string | null;
legally: string | null;
pros: string | null;
cons: string | null;
notablePeople: string | null;
links: string | null;
namedays: string | null;
namedaysComment: string | null;
approved: boolean;
base_id: string | null;
author: string | null;
@ -1335,16 +1335,16 @@ export interface NameRaw {
export class Name {
id: string;
name: string;
origin: string;
meaning: string;
usage: string;
legally: string;
origin: string | null;
meaning: string | null;
usage: string | null;
legally: string | null;
pros: string[];
cons: string[];
notablePeople: string[];
links: string[];
namedays: string[];
namedaysComment: string;
namedaysComment: string | null;
approved: boolean;
base: string | null;
author: string | null;