mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-08-04 03:27:05 -04:00
(refactor) migrate /api/names/** from express to h3
This commit is contained in:
parent
317758db01
commit
0299dbec0e
@ -6,7 +6,7 @@ import useConfig from '~/composables/useConfig.ts';
|
|||||||
import useDialogue from '~/composables/useDialogue.ts';
|
import useDialogue from '~/composables/useDialogue.ts';
|
||||||
import useHash from '~/composables/useHash.ts';
|
import useHash from '~/composables/useHash.ts';
|
||||||
import useSimpleHead from '~/composables/useSimpleHead.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';
|
import { buildDict } from '~/src/helpers.ts';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@ -21,7 +21,7 @@ useSimpleHead({
|
|||||||
const { handleHash, setHash } = useHash();
|
const { handleHash, setHash } = useHash();
|
||||||
|
|
||||||
const namesAsyncData = useAsyncData(async () => {
|
const namesAsyncData = useAsyncData(async () => {
|
||||||
const namesRaw = await $fetch<NameRaw[]>('/api/names');
|
const namesRaw = await $fetch('/api/names');
|
||||||
|
|
||||||
return buildDict(function* () {
|
return buildDict(function* () {
|
||||||
const sorted = namesRaw.sort((a, b) => {
|
const sorted = namesRaw.sort((a, b) => {
|
||||||
|
19
server/api/names/approve/[id].post.ts
Normal file
19
server/api/names/approve/[id].post.ts
Normal 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 });
|
||||||
|
});
|
26
server/api/names/hide/[id].post.ts
Normal file
26
server/api/names/hide/[id].post.ts
Normal 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 });
|
||||||
|
});
|
8
server/api/names/index.get.ts
Normal file
8
server/api/names/index.get.ts
Normal 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));
|
||||||
|
});
|
26
server/api/names/remove/[id].post.ts
Normal file
26
server/api/names/remove/[id].post.ts
Normal 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 });
|
||||||
|
});
|
44
server/api/names/submit.post.ts
Normal file
44
server/api/names/submit.post.ts
Normal 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');
|
||||||
|
});
|
@ -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
1
server/global.d.ts
vendored
@ -13,7 +13,6 @@ declare global {
|
|||||||
isGranted: (area?: string, locale?: string) => boolean;
|
isGranted: (area?: string, locale?: string) => boolean;
|
||||||
locales: Record<string, LocaleDescription>;
|
locales: Record<string, LocaleDescription>;
|
||||||
db: Database;
|
db: Database;
|
||||||
isUserAllowedToPost: () => Promise<boolean>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Response {
|
export interface Response {
|
||||||
|
@ -22,7 +22,6 @@ import discord from './express/discord.ts';
|
|||||||
import grantOverridesRoute from './express/grantOverrides.ts';
|
import grantOverridesRoute from './express/grantOverrides.ts';
|
||||||
import imagesRoute from './express/images.ts';
|
import imagesRoute from './express/images.ts';
|
||||||
import mfaRoute from './express/mfa.ts';
|
import mfaRoute from './express/mfa.ts';
|
||||||
import namesRoute from './express/names.ts';
|
|
||||||
import profileRoute from './express/profile.ts';
|
import profileRoute from './express/profile.ts';
|
||||||
import pronounceRoute from './express/pronounce.ts';
|
import pronounceRoute from './express/pronounce.ts';
|
||||||
import sentryRoute from './express/sentry.ts';
|
import sentryRoute from './express/sentry.ts';
|
||||||
@ -33,7 +32,6 @@ import { config } from './social.ts';
|
|||||||
|
|
||||||
import { closeAuditLogConnection } from '~/server/audit.ts';
|
import { closeAuditLogConnection } from '~/server/audit.ts';
|
||||||
import { getLocale } from '~/server/data.ts';
|
import { getLocale } from '~/server/data.ts';
|
||||||
import { isAllowedToPost } from '~/server/user.ts';
|
|
||||||
|
|
||||||
class StorageStore extends session.Store {
|
class StorageStore extends session.Store {
|
||||||
get(sid: string, callback: (err: unknown, session?: (session.SessionData | null)) => void): void {
|
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.isGranted = authentication.isGranted;
|
||||||
req.locales = buildLocaleList(locale, locale === '_');
|
req.locales = buildLocaleList(locale, locale === '_');
|
||||||
req.db = new LazyDatabase();
|
req.db = new LazyDatabase();
|
||||||
req.isUserAllowedToPost = (): Promise<boolean> => isAllowedToPost(req.db, req.user);
|
|
||||||
res.on('finish', async () => {
|
res.on('finish', async () => {
|
||||||
await req.db.close();
|
await req.db.close();
|
||||||
await closeAuditLogConnection();
|
await closeAuditLogConnection();
|
||||||
@ -159,7 +156,6 @@ router.use(adminRoute);
|
|||||||
router.use(mfaRoute);
|
router.use(mfaRoute);
|
||||||
router.use(pronounceRoute);
|
router.use(pronounceRoute);
|
||||||
router.use(censusRoute);
|
router.use(censusRoute);
|
||||||
router.use(namesRoute);
|
|
||||||
router.use(imagesRoute);
|
router.use(imagesRoute);
|
||||||
router.use(calendarRoute);
|
router.use(calendarRoute);
|
||||||
router.use(translationsRoute);
|
router.use(translationsRoute);
|
||||||
|
63
server/names.ts
Normal file
63
server/names.ts
Normal 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);
|
||||||
|
};
|
@ -1317,16 +1317,16 @@ export class TermsEntry implements Entry {
|
|||||||
export interface NameRaw {
|
export interface NameRaw {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
origin: string;
|
origin: string | null;
|
||||||
meaning: string;
|
meaning: string | null;
|
||||||
usage: string;
|
usage: string | null;
|
||||||
legally: string;
|
legally: string | null;
|
||||||
pros: string;
|
pros: string | null;
|
||||||
cons: string;
|
cons: string | null;
|
||||||
notablePeople: string;
|
notablePeople: string | null;
|
||||||
links: string;
|
links: string | null;
|
||||||
namedays: string;
|
namedays: string | null;
|
||||||
namedaysComment: string;
|
namedaysComment: string | null;
|
||||||
approved: boolean;
|
approved: boolean;
|
||||||
base_id: string | null;
|
base_id: string | null;
|
||||||
author: string | null;
|
author: string | null;
|
||||||
@ -1335,16 +1335,16 @@ export interface NameRaw {
|
|||||||
export class Name {
|
export class Name {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
origin: string;
|
origin: string | null;
|
||||||
meaning: string;
|
meaning: string | null;
|
||||||
usage: string;
|
usage: string | null;
|
||||||
legally: string;
|
legally: string | null;
|
||||||
pros: string[];
|
pros: string[];
|
||||||
cons: string[];
|
cons: string[];
|
||||||
notablePeople: string[];
|
notablePeople: string[];
|
||||||
links: string[];
|
links: string[];
|
||||||
namedays: string[];
|
namedays: string[];
|
||||||
namedaysComment: string;
|
namedaysComment: string | null;
|
||||||
approved: boolean;
|
approved: boolean;
|
||||||
base: string | null;
|
base: string | null;
|
||||||
author: string | null;
|
author: string | null;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user