feat(i18n): add full-stack internationalization support
Add languages and entity_translations tables to database schema Implement localized queries and translation management in backend Integrate frontend i18n and add translation UI components
This commit is contained in:
@@ -3,6 +3,52 @@ CREATE TABLE IF NOT EXISTS environments (
|
||||
name text NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS languages (
|
||||
code text PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
is_default boolean NOT NULL DEFAULT false,
|
||||
sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0),
|
||||
CHECK (code ~ '^[a-z]{2}(-[A-Z]{2})?$'),
|
||||
CHECK (length(name) BETWEEN 1 AND 80)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS languages_single_default_idx
|
||||
ON languages (is_default)
|
||||
WHERE is_default = true;
|
||||
|
||||
INSERT INTO languages (code, name, enabled, is_default, sort_order)
|
||||
VALUES
|
||||
('en', 'English', true, true, 10),
|
||||
('zh-CN', '简体中文', true, false, 20)
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS entity_translations (
|
||||
entity_type text NOT NULL CHECK (
|
||||
entity_type IN (
|
||||
'pokemon',
|
||||
'skills',
|
||||
'environments',
|
||||
'favorite-things',
|
||||
'item-categories',
|
||||
'item-usages',
|
||||
'acquisition-methods',
|
||||
'items',
|
||||
'maps',
|
||||
'habitats',
|
||||
'daily-checklist-items'
|
||||
)
|
||||
),
|
||||
entity_id integer NOT NULL,
|
||||
locale text NOT NULL REFERENCES languages(code) ON DELETE CASCADE,
|
||||
field_name text NOT NULL CHECK (field_name IN ('name', 'title')),
|
||||
value text NOT NULL,
|
||||
PRIMARY KEY (entity_type, entity_id, locale, field_name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS entity_translations_lookup_idx
|
||||
ON entity_translations (entity_type, entity_id, field_name, locale);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
email text NOT NULL UNIQUE,
|
||||
|
||||
@@ -7,6 +7,7 @@ const scrypt = promisify(scryptCallback);
|
||||
const passwordKeyLength = 64;
|
||||
const verificationTokenHours = 24;
|
||||
const sessionDays = 30;
|
||||
const defaultLocale = 'en';
|
||||
|
||||
type DbClient = PoolClient;
|
||||
|
||||
@@ -23,6 +24,22 @@ type LoginUserRow = UserRow & {
|
||||
password_hash: string;
|
||||
};
|
||||
|
||||
type AuthMessageKey =
|
||||
| 'emailRequired'
|
||||
| 'invalidEmail'
|
||||
| 'displayNameRequired'
|
||||
| 'displayNameLength'
|
||||
| 'passwordLength'
|
||||
| 'invalidToken'
|
||||
| 'emailAlreadyRegistered'
|
||||
| 'checkVerificationEmail'
|
||||
| 'emailVerified'
|
||||
| 'invalidCredentials'
|
||||
| 'verifyEmailFirst'
|
||||
| 'emailSubject'
|
||||
| 'emailHtml'
|
||||
| 'emailText';
|
||||
|
||||
export type AuthUser = {
|
||||
id: number;
|
||||
email: string;
|
||||
@@ -36,43 +53,87 @@ function statusError(message: string, statusCode: number): StatusError {
|
||||
return error;
|
||||
}
|
||||
|
||||
function cleanEmail(value: unknown): string {
|
||||
function authMessage(locale: string, key: AuthMessageKey, params: Record<string, string | number> = {}): string {
|
||||
const messages: Record<string, Record<AuthMessageKey, string>> = {
|
||||
en: {
|
||||
emailRequired: 'Email is required',
|
||||
invalidEmail: 'Email format is invalid',
|
||||
displayNameRequired: 'Display name is required',
|
||||
displayNameLength: 'Display name must be 1 to 40 characters',
|
||||
passwordLength: 'Password must be at least 8 characters',
|
||||
invalidToken: 'The verification link is invalid or expired',
|
||||
emailAlreadyRegistered: 'This email is already registered',
|
||||
checkVerificationEmail: 'Please check your verification email',
|
||||
emailVerified: 'Email verified',
|
||||
invalidCredentials: 'Email or password is incorrect',
|
||||
verifyEmailFirst: 'Please complete email verification first',
|
||||
emailSubject: 'Verify your Pokopia Wiki email',
|
||||
emailHtml:
|
||||
'<p>Open the link below to verify your email:</p><p><a href="{url}">Verify email</a></p><p>The link expires in {hours} hours.</p>',
|
||||
emailText: 'Open this link to verify your Pokopia Wiki email: {url}\nThe link expires in {hours} hours.'
|
||||
},
|
||||
'zh-CN': {
|
||||
emailRequired: '请输入邮箱',
|
||||
invalidEmail: '邮箱格式不正确',
|
||||
displayNameRequired: '请输入显示名',
|
||||
displayNameLength: '显示名长度需为 1 到 40 个字符',
|
||||
passwordLength: '密码至少需要 8 个字符',
|
||||
invalidToken: '验证链接无效或已过期',
|
||||
emailAlreadyRegistered: '该邮箱已注册',
|
||||
checkVerificationEmail: '请查收验证邮件',
|
||||
emailVerified: '邮箱已验证',
|
||||
invalidCredentials: '邮箱或密码不正确',
|
||||
verifyEmailFirst: '请先完成邮箱验证',
|
||||
emailSubject: '验证你的 Pokopia Wiki 邮箱',
|
||||
emailHtml: '<p>请点击下面的链接完成邮箱验证:</p><p><a href="{url}">验证邮箱</a></p><p>链接将在 {hours} 小时后失效。</p>',
|
||||
emailText: '请打开以下链接完成 Pokopia Wiki 邮箱验证:{url}\n链接将在 {hours} 小时后失效。'
|
||||
}
|
||||
};
|
||||
|
||||
let message = messages[locale]?.[key] ?? messages[defaultLocale][key];
|
||||
for (const [paramKey, paramValue] of Object.entries(params)) {
|
||||
message = message.replaceAll(`{${paramKey}}`, String(paramValue));
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function cleanEmail(value: unknown, locale: string): string {
|
||||
if (typeof value !== 'string') {
|
||||
throw statusError('请输入邮箱', 400);
|
||||
throw statusError(authMessage(locale, 'emailRequired'), 400);
|
||||
}
|
||||
|
||||
const email = value.trim().toLowerCase();
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
throw statusError('邮箱格式不正确', 400);
|
||||
throw statusError(authMessage(locale, 'invalidEmail'), 400);
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
function cleanDisplayName(value: unknown): string {
|
||||
function cleanDisplayName(value: unknown, locale: string): string {
|
||||
if (typeof value !== 'string') {
|
||||
throw statusError('请输入显示名', 400);
|
||||
throw statusError(authMessage(locale, 'displayNameRequired'), 400);
|
||||
}
|
||||
|
||||
const displayName = value.trim();
|
||||
if (displayName.length < 1 || displayName.length > 40) {
|
||||
throw statusError('显示名长度需为 1 到 40 个字符', 400);
|
||||
throw statusError(authMessage(locale, 'displayNameLength'), 400);
|
||||
}
|
||||
|
||||
return displayName;
|
||||
}
|
||||
|
||||
function cleanPassword(value: unknown): string {
|
||||
function cleanPassword(value: unknown, locale: string): string {
|
||||
if (typeof value !== 'string' || value.length < 8) {
|
||||
throw statusError('密码至少需要 8 个字符', 400);
|
||||
throw statusError(authMessage(locale, 'passwordLength'), 400);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function cleanToken(value: unknown): string {
|
||||
function cleanToken(value: unknown, locale: string): string {
|
||||
if (typeof value !== 'string' || value.trim().length < 32) {
|
||||
throw statusError('验证链接无效或已过期', 400);
|
||||
throw statusError(authMessage(locale, 'invalidToken'), 400);
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
@@ -155,7 +216,7 @@ function buildVerificationUrl(token: string): string {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async function sendVerificationEmail(email: string, token: string): Promise<void> {
|
||||
async function sendVerificationEmail(email: string, token: string, locale: string): Promise<void> {
|
||||
const { apiKey, from } = getEmailConfig();
|
||||
const verificationUrl = buildVerificationUrl(token);
|
||||
const response = await fetch('https://api.resend.com/emails', {
|
||||
@@ -167,9 +228,9 @@ async function sendVerificationEmail(email: string, token: string): Promise<void
|
||||
body: JSON.stringify({
|
||||
from,
|
||||
to: [email],
|
||||
subject: '验证你的 Pokopia Wiki 邮箱',
|
||||
html: `<p>请点击下面的链接完成邮箱验证:</p><p><a href="${verificationUrl}">验证邮箱</a></p><p>链接将在 ${verificationTokenHours} 小时后失效。</p>`,
|
||||
text: `请打开以下链接完成 Pokopia Wiki 邮箱验证:${verificationUrl}\n链接将在 ${verificationTokenHours} 小时后失效。`
|
||||
subject: authMessage(locale, 'emailSubject'),
|
||||
html: authMessage(locale, 'emailHtml', { url: verificationUrl, hours: verificationTokenHours }),
|
||||
text: authMessage(locale, 'emailText', { url: verificationUrl, hours: verificationTokenHours })
|
||||
})
|
||||
});
|
||||
|
||||
@@ -179,10 +240,10 @@ async function sendVerificationEmail(email: string, token: string): Promise<void
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerUser(payload: Record<string, unknown>) {
|
||||
const email = cleanEmail(payload.email);
|
||||
const displayName = cleanDisplayName(payload.displayName);
|
||||
const password = cleanPassword(payload.password);
|
||||
export async function registerUser(payload: Record<string, unknown>, locale = defaultLocale) {
|
||||
const email = cleanEmail(payload.email, locale);
|
||||
const displayName = cleanDisplayName(payload.displayName, locale);
|
||||
const password = cleanPassword(payload.password, locale);
|
||||
const passwordHash = await hashPassword(password);
|
||||
const verificationToken = createPlainToken();
|
||||
const verificationTokenHash = hashToken(verificationToken);
|
||||
@@ -195,7 +256,7 @@ export async function registerUser(payload: Record<string, unknown>) {
|
||||
);
|
||||
|
||||
if (existingUser?.email_verified_at) {
|
||||
throw statusError('该邮箱已注册', 409);
|
||||
throw statusError(authMessage(locale, 'emailAlreadyRegistered'), 409);
|
||||
}
|
||||
|
||||
const user = existingUser
|
||||
@@ -233,12 +294,12 @@ export async function registerUser(payload: Record<string, unknown>) {
|
||||
);
|
||||
});
|
||||
|
||||
await sendVerificationEmail(email, verificationToken);
|
||||
return { message: '请查收验证邮件' };
|
||||
await sendVerificationEmail(email, verificationToken, locale);
|
||||
return { message: authMessage(locale, 'checkVerificationEmail') };
|
||||
}
|
||||
|
||||
export async function verifyEmail(payload: Record<string, unknown>) {
|
||||
const token = cleanToken(payload.token);
|
||||
export async function verifyEmail(payload: Record<string, unknown>, locale = defaultLocale) {
|
||||
const token = cleanToken(payload.token, locale);
|
||||
const tokenHash = hashToken(token);
|
||||
|
||||
return withTransaction(async (client) => {
|
||||
@@ -256,7 +317,7 @@ export async function verifyEmail(payload: Record<string, unknown>) {
|
||||
);
|
||||
|
||||
if (!tokenRow) {
|
||||
throw statusError('验证链接无效或已过期', 400);
|
||||
throw statusError(authMessage(locale, 'invalidToken'), 400);
|
||||
}
|
||||
|
||||
const user = await clientQueryOne<UserRow>(
|
||||
@@ -271,31 +332,31 @@ export async function verifyEmail(payload: Record<string, unknown>) {
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
throw statusError('验证链接无效或已过期', 400);
|
||||
throw statusError(authMessage(locale, 'invalidToken'), 400);
|
||||
}
|
||||
|
||||
await client.query('UPDATE email_verification_tokens SET used_at = now() WHERE user_id = $1 AND used_at IS NULL', [
|
||||
user.id
|
||||
]);
|
||||
|
||||
return { message: '邮箱已验证', user: toPublicUser(user) };
|
||||
return { message: authMessage(locale, 'emailVerified'), user: toPublicUser(user) };
|
||||
});
|
||||
}
|
||||
|
||||
export async function loginUser(payload: Record<string, unknown>) {
|
||||
const email = cleanEmail(payload.email);
|
||||
const password = cleanPassword(payload.password);
|
||||
export async function loginUser(payload: Record<string, unknown>, locale = defaultLocale) {
|
||||
const email = cleanEmail(payload.email, locale);
|
||||
const password = cleanPassword(payload.password, locale);
|
||||
const user = await queryOne<LoginUserRow>(
|
||||
'SELECT id, email, display_name, email_verified_at, password_hash FROM users WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (!user || !(await verifyPassword(password, user.password_hash))) {
|
||||
throw statusError('邮箱或密码不正确', 401);
|
||||
throw statusError(authMessage(locale, 'invalidCredentials'), 401);
|
||||
}
|
||||
|
||||
if (!user.email_verified_at) {
|
||||
throw statusError('请先完成邮箱验证', 403);
|
||||
throw statusError(authMessage(locale, 'verifyEmailFirst'), 403);
|
||||
}
|
||||
|
||||
const sessionToken = createPlainToken();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,19 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { getUserBySessionToken, loginUser, logoutSession, registerUser, verifyEmail, type AuthUser } from './auth.ts';
|
||||
import { initializeDatabase, pool } from './db.ts';
|
||||
import {
|
||||
cleanLocale,
|
||||
createConfig,
|
||||
createDailyChecklistItem,
|
||||
createHabitat,
|
||||
createItem,
|
||||
createLanguage,
|
||||
createPokemon,
|
||||
createRecipe,
|
||||
deleteConfig,
|
||||
deleteDailyChecklistItem,
|
||||
deleteHabitat,
|
||||
deleteItem,
|
||||
deleteLanguage,
|
||||
deletePokemon,
|
||||
deleteRecipe,
|
||||
getHabitat,
|
||||
@@ -26,13 +29,16 @@ import {
|
||||
listDailyChecklistItems,
|
||||
listHabitats,
|
||||
listItems,
|
||||
listLanguages,
|
||||
listPokemon,
|
||||
listRecipes,
|
||||
reorderDailyChecklistItems,
|
||||
reorderLanguages,
|
||||
updateConfig,
|
||||
updateDailyChecklistItem,
|
||||
updateHabitat,
|
||||
updateItem,
|
||||
updateLanguage,
|
||||
updatePokemon,
|
||||
updateRecipe
|
||||
} from './queries.ts';
|
||||
@@ -42,24 +48,25 @@ const app = Fastify({
|
||||
});
|
||||
|
||||
await app.register(cors, {
|
||||
allowedHeaders: ['Authorization', 'Content-Type'],
|
||||
allowedHeaders: ['Authorization', 'Content-Type', 'X-Locale'],
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
origin: process.env.FRONTEND_ORIGIN ?? true
|
||||
});
|
||||
|
||||
app.setErrorHandler(async (error, _request, reply) => {
|
||||
const pgError = error as Error & { code?: string; constraint?: string; detail?: string; statusCode?: number };
|
||||
const locale = requestLocale(_request);
|
||||
|
||||
if (pgError.code === '23503') {
|
||||
return reply.code(409).send({ message: '引用的数据不存在,或当前记录正在被使用' });
|
||||
return reply.code(409).send({ message: serverMessage(locale, 'foreignKey') });
|
||||
}
|
||||
|
||||
if (pgError.code === '23505') {
|
||||
return reply.code(409).send({ message: '同名或相同 ID 的记录已存在' });
|
||||
return reply.code(409).send({ message: serverMessage(locale, 'duplicate') });
|
||||
}
|
||||
|
||||
if (pgError.code === '23514') {
|
||||
return reply.code(400).send({ message: '字段值不合法' });
|
||||
return reply.code(400).send({ message: serverMessage(locale, 'invalidField') });
|
||||
}
|
||||
|
||||
if (pgError.statusCode && pgError.statusCode < 500) {
|
||||
@@ -67,7 +74,7 @@ app.setErrorHandler(async (error, _request, reply) => {
|
||||
}
|
||||
|
||||
app.log.error(error);
|
||||
return reply.code(500).send({ message: '服务器错误' });
|
||||
return reply.code(500).send({ message: serverMessage(locale, 'serverError') });
|
||||
});
|
||||
|
||||
app.get('/health', async () => ({ ok: true }));
|
||||
@@ -77,17 +84,48 @@ function getBearerToken(authorization: string | undefined): string | null {
|
||||
return scheme === 'Bearer' && token ? token : null;
|
||||
}
|
||||
|
||||
function requestLocale(request: FastifyRequest): string {
|
||||
const query = request.query as Record<string, string | string[] | undefined>;
|
||||
const queryLocale = Array.isArray(query.locale) ? query.locale[0] : query.locale;
|
||||
const headerLocale = request.headers['x-locale'];
|
||||
return cleanLocale(queryLocale ?? (Array.isArray(headerLocale) ? headerLocale[0] : headerLocale));
|
||||
}
|
||||
|
||||
function serverMessage(locale: string, key: 'foreignKey' | 'duplicate' | 'invalidField' | 'serverError' | 'loginRequired' | 'verifyEmailFirst'): string {
|
||||
const messages = {
|
||||
en: {
|
||||
foreignKey: 'Referenced data does not exist or the record is currently in use',
|
||||
duplicate: 'A record with the same name or ID already exists',
|
||||
invalidField: 'Field value is invalid',
|
||||
serverError: 'Server error',
|
||||
loginRequired: 'Please log in first',
|
||||
verifyEmailFirst: 'Please complete email verification first'
|
||||
},
|
||||
'zh-CN': {
|
||||
foreignKey: '引用的数据不存在,或当前记录正在被使用',
|
||||
duplicate: '同名或相同 ID 的记录已存在',
|
||||
invalidField: '字段值不合法',
|
||||
serverError: '服务器错误',
|
||||
loginRequired: '请先登录',
|
||||
verifyEmailFirst: '请先完成邮箱验证'
|
||||
}
|
||||
};
|
||||
|
||||
return messages[locale as keyof typeof messages]?.[key] ?? messages.en[key];
|
||||
}
|
||||
|
||||
async function requireVerifiedUser(request: FastifyRequest, reply: FastifyReply): Promise<AuthUser | null> {
|
||||
const token = getBearerToken(request.headers.authorization);
|
||||
const user = token ? await getUserBySessionToken(token) : null;
|
||||
const locale = requestLocale(request);
|
||||
|
||||
if (!user) {
|
||||
reply.code(401).send({ message: '请先登录' });
|
||||
reply.code(401).send({ message: serverMessage(locale, 'loginRequired') });
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!user.emailVerified) {
|
||||
reply.code(403).send({ message: '请先完成邮箱验证' });
|
||||
reply.code(403).send({ message: serverMessage(locale, 'verifyEmailFirst') });
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -95,19 +133,19 @@ async function requireVerifiedUser(request: FastifyRequest, reply: FastifyReply)
|
||||
}
|
||||
|
||||
app.post('/api/auth/register', async (request, reply) =>
|
||||
reply.code(201).send(await registerUser(request.body as Record<string, unknown>))
|
||||
reply.code(201).send(await registerUser(request.body as Record<string, unknown>, requestLocale(request)))
|
||||
);
|
||||
|
||||
app.post('/api/auth/verify-email', async (request) => verifyEmail(request.body as Record<string, unknown>));
|
||||
app.post('/api/auth/verify-email', async (request) => verifyEmail(request.body as Record<string, unknown>, requestLocale(request)));
|
||||
|
||||
app.post('/api/auth/login', async (request) => loginUser(request.body as Record<string, unknown>));
|
||||
app.post('/api/auth/login', async (request) => loginUser(request.body as Record<string, unknown>, requestLocale(request)));
|
||||
|
||||
app.get('/api/auth/me', async (request, reply) => {
|
||||
const token = getBearerToken(request.headers.authorization);
|
||||
const user = token ? await getUserBySessionToken(token) : null;
|
||||
|
||||
if (!user) {
|
||||
return reply.code(401).send({ message: '请先登录' });
|
||||
return reply.code(401).send({ message: serverMessage(requestLocale(request), 'loginRequired') });
|
||||
}
|
||||
|
||||
return { user };
|
||||
@@ -122,15 +160,19 @@ app.post('/api/auth/logout', async (request, reply) => {
|
||||
return reply.code(204).send();
|
||||
});
|
||||
|
||||
app.get('/api/options', async () => getOptions());
|
||||
app.get('/api/languages', async () => listLanguages());
|
||||
|
||||
app.get('/api/daily-checklist', async () => listDailyChecklistItems());
|
||||
app.get('/api/options', async (request) => getOptions(requestLocale(request)));
|
||||
|
||||
app.get('/api/pokemon', async (request) => listPokemon(request.query as Record<string, string | string[] | undefined>));
|
||||
app.get('/api/daily-checklist', async (request) => listDailyChecklistItems(requestLocale(request)));
|
||||
|
||||
app.get('/api/pokemon', async (request) =>
|
||||
listPokemon(request.query as Record<string, string | string[] | undefined>, requestLocale(request))
|
||||
);
|
||||
|
||||
app.get('/api/pokemon/:id', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const pokemon = await getPokemon(Number(id));
|
||||
const pokemon = await getPokemon(Number(id), requestLocale(request));
|
||||
|
||||
if (!pokemon) {
|
||||
return reply.code(404).send({ message: 'Not found' });
|
||||
@@ -141,7 +183,9 @@ app.get('/api/pokemon/:id', async (request, reply) => {
|
||||
|
||||
app.post('/api/pokemon', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
return user ? reply.code(201).send(await createPokemon(request.body as Record<string, unknown>, user.id)) : undefined;
|
||||
return user
|
||||
? reply.code(201).send(await createPokemon(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
||||
: undefined;
|
||||
});
|
||||
|
||||
app.put('/api/pokemon/:id', async (request, reply) => {
|
||||
@@ -150,7 +194,7 @@ app.put('/api/pokemon/:id', async (request, reply) => {
|
||||
return;
|
||||
}
|
||||
const { id } = request.params as { id: string };
|
||||
const pokemon = await updatePokemon(Number(id), request.body as Record<string, unknown>, user.id);
|
||||
const pokemon = await updatePokemon(Number(id), request.body as Record<string, unknown>, user.id, requestLocale(request));
|
||||
|
||||
if (!pokemon) {
|
||||
return reply.code(404).send({ message: 'Not found' });
|
||||
@@ -169,11 +213,11 @@ app.delete('/api/pokemon/:id', async (request, reply) => {
|
||||
return deleted ? reply.code(204).send() : reply.code(404).send({ message: 'Not found' });
|
||||
});
|
||||
|
||||
app.get('/api/habitats', async () => listHabitats());
|
||||
app.get('/api/habitats', async (request) => listHabitats(requestLocale(request)));
|
||||
|
||||
app.get('/api/habitats/:id', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const habitat = await getHabitat(Number(id));
|
||||
const habitat = await getHabitat(Number(id), requestLocale(request));
|
||||
|
||||
if (!habitat) {
|
||||
return reply.code(404).send({ message: 'Not found' });
|
||||
@@ -184,7 +228,9 @@ app.get('/api/habitats/:id', async (request, reply) => {
|
||||
|
||||
app.post('/api/habitats', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
return user ? reply.code(201).send(await createHabitat(request.body as Record<string, unknown>, user.id)) : undefined;
|
||||
return user
|
||||
? reply.code(201).send(await createHabitat(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
||||
: undefined;
|
||||
});
|
||||
|
||||
app.put('/api/habitats/:id', async (request, reply) => {
|
||||
@@ -193,7 +239,7 @@ app.put('/api/habitats/:id', async (request, reply) => {
|
||||
return;
|
||||
}
|
||||
const { id } = request.params as { id: string };
|
||||
const habitat = await updateHabitat(Number(id), request.body as Record<string, unknown>, user.id);
|
||||
const habitat = await updateHabitat(Number(id), request.body as Record<string, unknown>, user.id, requestLocale(request));
|
||||
|
||||
if (!habitat) {
|
||||
return reply.code(404).send({ message: 'Not found' });
|
||||
@@ -212,11 +258,13 @@ app.delete('/api/habitats/:id', async (request, reply) => {
|
||||
return deleted ? reply.code(204).send() : reply.code(404).send({ message: 'Not found' });
|
||||
});
|
||||
|
||||
app.get('/api/items', async (request) => listItems(request.query as Record<string, string | string[] | undefined>));
|
||||
app.get('/api/items', async (request) =>
|
||||
listItems(request.query as Record<string, string | string[] | undefined>, requestLocale(request))
|
||||
);
|
||||
|
||||
app.get('/api/items/:id', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const item = await getItem(Number(id));
|
||||
const item = await getItem(Number(id), requestLocale(request));
|
||||
|
||||
if (!item) {
|
||||
return reply.code(404).send({ message: 'Not found' });
|
||||
@@ -227,7 +275,9 @@ app.get('/api/items/:id', async (request, reply) => {
|
||||
|
||||
app.post('/api/items', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
return user ? reply.code(201).send(await createItem(request.body as Record<string, unknown>, user.id)) : undefined;
|
||||
return user
|
||||
? reply.code(201).send(await createItem(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
||||
: undefined;
|
||||
});
|
||||
|
||||
app.put('/api/items/:id', async (request, reply) => {
|
||||
@@ -236,7 +286,7 @@ app.put('/api/items/:id', async (request, reply) => {
|
||||
return;
|
||||
}
|
||||
const { id } = request.params as { id: string };
|
||||
const item = await updateItem(Number(id), request.body as Record<string, unknown>, user.id);
|
||||
const item = await updateItem(Number(id), request.body as Record<string, unknown>, user.id, requestLocale(request));
|
||||
|
||||
if (!item) {
|
||||
return reply.code(404).send({ message: 'Not found' });
|
||||
@@ -255,11 +305,13 @@ app.delete('/api/items/:id', async (request, reply) => {
|
||||
return deleted ? reply.code(204).send() : reply.code(404).send({ message: 'Not found' });
|
||||
});
|
||||
|
||||
app.get('/api/recipes', async (request) => listRecipes(request.query as Record<string, string | string[] | undefined>));
|
||||
app.get('/api/recipes', async (request) =>
|
||||
listRecipes(request.query as Record<string, string | string[] | undefined>, requestLocale(request))
|
||||
);
|
||||
|
||||
app.get('/api/recipes/:id', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const recipe = await getRecipe(Number(id));
|
||||
const recipe = await getRecipe(Number(id), requestLocale(request));
|
||||
|
||||
if (!recipe) {
|
||||
return reply.code(404).send({ message: 'Not found' });
|
||||
@@ -270,7 +322,9 @@ app.get('/api/recipes/:id', async (request, reply) => {
|
||||
|
||||
app.post('/api/recipes', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
return user ? reply.code(201).send(await createRecipe(request.body as Record<string, unknown>, user.id)) : undefined;
|
||||
return user
|
||||
? reply.code(201).send(await createRecipe(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
||||
: undefined;
|
||||
});
|
||||
|
||||
app.put('/api/recipes/:id', async (request, reply) => {
|
||||
@@ -279,7 +333,7 @@ app.put('/api/recipes/:id', async (request, reply) => {
|
||||
return;
|
||||
}
|
||||
const { id } = request.params as { id: string };
|
||||
const recipe = await updateRecipe(Number(id), request.body as Record<string, unknown>, user.id);
|
||||
const recipe = await updateRecipe(Number(id), request.body as Record<string, unknown>, user.id, requestLocale(request));
|
||||
|
||||
if (!recipe) {
|
||||
return reply.code(404).send({ message: 'Not found' });
|
||||
@@ -300,12 +354,16 @@ app.delete('/api/recipes/:id', async (request, reply) => {
|
||||
|
||||
app.post('/api/admin/daily-checklist', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
return user ? reply.code(201).send(await createDailyChecklistItem(request.body as Record<string, unknown>, user.id)) : undefined;
|
||||
return user
|
||||
? reply
|
||||
.code(201)
|
||||
.send(await createDailyChecklistItem(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
||||
: undefined;
|
||||
});
|
||||
|
||||
app.put('/api/admin/daily-checklist/order', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
return user ? reorderDailyChecklistItems(request.body as Record<string, unknown>, user.id) : undefined;
|
||||
return user ? reorderDailyChecklistItems(request.body as Record<string, unknown>, user.id, requestLocale(request)) : undefined;
|
||||
});
|
||||
|
||||
app.put('/api/admin/daily-checklist/:id', async (request, reply) => {
|
||||
@@ -314,7 +372,12 @@ app.put('/api/admin/daily-checklist/:id', async (request, reply) => {
|
||||
return;
|
||||
}
|
||||
const { id } = request.params as { id: string };
|
||||
const item = await updateDailyChecklistItem(Number(id), request.body as Record<string, unknown>, user.id);
|
||||
const item = await updateDailyChecklistItem(
|
||||
Number(id),
|
||||
request.body as Record<string, unknown>,
|
||||
user.id,
|
||||
requestLocale(request)
|
||||
);
|
||||
return item ? item : reply.code(404).send({ message: 'Not found' });
|
||||
});
|
||||
|
||||
@@ -328,6 +391,40 @@ app.delete('/api/admin/daily-checklist/:id', async (request, reply) => {
|
||||
return deleted ? reply.code(204).send() : reply.code(404).send({ message: 'Not found' });
|
||||
});
|
||||
|
||||
app.get('/api/admin/languages', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
return user ? listLanguages(true) : undefined;
|
||||
});
|
||||
|
||||
app.post('/api/admin/languages', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
return user ? reply.code(201).send(await createLanguage(request.body as Record<string, unknown>)) : undefined;
|
||||
});
|
||||
|
||||
app.put('/api/admin/languages/order', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
return user ? reorderLanguages(request.body as Record<string, unknown>) : undefined;
|
||||
});
|
||||
|
||||
app.put('/api/admin/languages/:code', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
const { code } = request.params as { code: string };
|
||||
return updateLanguage(code, request.body as Record<string, unknown>);
|
||||
});
|
||||
|
||||
app.delete('/api/admin/languages/:code', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
const { code } = request.params as { code: string };
|
||||
const deleted = await deleteLanguage(code);
|
||||
return deleted ? reply.code(204).send() : reply.code(404).send({ message: 'Not found' });
|
||||
});
|
||||
|
||||
app.get('/api/admin/config/:type', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
if (!user) {
|
||||
@@ -337,7 +434,7 @@ app.get('/api/admin/config/:type', async (request, reply) => {
|
||||
if (!isConfigType(type)) {
|
||||
return reply.code(404).send({ message: 'Not found' });
|
||||
}
|
||||
return listConfig(type);
|
||||
return listConfig(type, requestLocale(request));
|
||||
});
|
||||
|
||||
app.post('/api/admin/config/:type', async (request, reply) => {
|
||||
@@ -349,7 +446,9 @@ app.post('/api/admin/config/:type', async (request, reply) => {
|
||||
if (!isConfigType(type)) {
|
||||
return reply.code(404).send({ message: 'Not found' });
|
||||
}
|
||||
return reply.code(201).send(await createConfig(type, request.body as Record<string, unknown>, user.id));
|
||||
return reply
|
||||
.code(201)
|
||||
.send(await createConfig(type, request.body as Record<string, unknown>, user.id, requestLocale(request)));
|
||||
});
|
||||
|
||||
app.put('/api/admin/config/:type/:id', async (request, reply) => {
|
||||
@@ -361,7 +460,7 @@ app.put('/api/admin/config/:type/:id', async (request, reply) => {
|
||||
if (!isConfigType(type)) {
|
||||
return reply.code(404).send({ message: 'Not found' });
|
||||
}
|
||||
const config = await updateConfig(type, Number(id), request.body as Record<string, unknown>, user.id);
|
||||
const config = await updateConfig(type, Number(id), request.body as Record<string, unknown>, user.id, requestLocale(request));
|
||||
return config ? config : reply.code(404).send({ message: 'Not found' });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user