|
|
|
@@ -1,8 +1,10 @@
|
|
|
|
import cors from '@fastify/cors';
|
|
|
|
import cors from '@fastify/cors';
|
|
|
|
import multipart, { type MultipartFile } from '@fastify/multipart';
|
|
|
|
import multipart, { type MultipartFile } from '@fastify/multipart';
|
|
|
|
|
|
|
|
import rateLimit from '@fastify/rate-limit';
|
|
|
|
import fastifyStatic from '@fastify/static';
|
|
|
|
import fastifyStatic from '@fastify/static';
|
|
|
|
import Fastify from 'fastify';
|
|
|
|
import Fastify from 'fastify';
|
|
|
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
|
|
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
|
|
|
|
|
|
|
import { createHash } from 'node:crypto';
|
|
|
|
import { mkdir } from 'node:fs/promises';
|
|
|
|
import { mkdir } from 'node:fs/promises';
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
changeCurrentUserPassword,
|
|
|
|
changeCurrentUserPassword,
|
|
|
|
@@ -111,7 +113,8 @@ import {
|
|
|
|
} from './uploads.ts';
|
|
|
|
} from './uploads.ts';
|
|
|
|
|
|
|
|
|
|
|
|
const app = Fastify({
|
|
|
|
const app = Fastify({
|
|
|
|
logger: true
|
|
|
|
logger: true,
|
|
|
|
|
|
|
|
trustProxy: process.env.TRUST_PROXY === 'true'
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await app.register(cors, {
|
|
|
|
await app.register(cors, {
|
|
|
|
@@ -120,6 +123,11 @@ await app.register(cors, {
|
|
|
|
origin: process.env.FRONTEND_ORIGIN ?? true
|
|
|
|
origin: process.env.FRONTEND_ORIGIN ?? true
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await app.register(rateLimit, {
|
|
|
|
|
|
|
|
global: false,
|
|
|
|
|
|
|
|
hook: 'preHandler'
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await mkdir(uploadRoot, { recursive: true });
|
|
|
|
await mkdir(uploadRoot, { recursive: true });
|
|
|
|
await app.register(multipart, {
|
|
|
|
await app.register(multipart, {
|
|
|
|
limits: {
|
|
|
|
limits: {
|
|
|
|
@@ -149,6 +157,10 @@ app.setErrorHandler(async (error, _request, reply) => {
|
|
|
|
return reply.code(400).send({ message: await serverMessage(locale, 'invalidField') });
|
|
|
|
return reply.code(400).send({ message: await serverMessage(locale, 'invalidField') });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (pgError.statusCode === 429) {
|
|
|
|
|
|
|
|
return reply.code(429).send({ message: await serverMessage(locale, 'rateLimited') });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pgError.statusCode && pgError.statusCode < 500) {
|
|
|
|
if (pgError.statusCode && pgError.statusCode < 500) {
|
|
|
|
return reply.code(pgError.statusCode).send({ message: await localizedStatusMessage(locale, pgError.message) });
|
|
|
|
return reply.code(pgError.statusCode).send({ message: await localizedStatusMessage(locale, pgError.message) });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -182,10 +194,243 @@ function serverMessage(
|
|
|
|
| 'verifyEmailFirst'
|
|
|
|
| 'verifyEmailFirst'
|
|
|
|
| 'permissionDenied'
|
|
|
|
| 'permissionDenied'
|
|
|
|
| 'notFound'
|
|
|
|
| 'notFound'
|
|
|
|
|
|
|
|
| 'rateLimited'
|
|
|
|
): Promise<string> {
|
|
|
|
): Promise<string> {
|
|
|
|
return systemMessage(locale, `server.errors.${key}`);
|
|
|
|
return systemMessage(locale, `server.errors.${key}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type RateLimitCheck = ReturnType<typeof app.createRateLimit>;
|
|
|
|
|
|
|
|
type RateLimitResult = Awaited<ReturnType<RateLimitCheck>>;
|
|
|
|
|
|
|
|
type RateLimitPolicy = 'accountWrite' | 'adminWrite' | 'communityReaction' | 'communityWrite' | 'fetch' | 'upload' | 'wikiWrite';
|
|
|
|
|
|
|
|
type RateLimitedRequest = FastifyRequest & {
|
|
|
|
|
|
|
|
rateLimitUserId?: number;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function hashRateLimitPart(value: string): string {
|
|
|
|
|
|
|
|
return createHash('sha256').update(value).digest('hex').slice(0, 32);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function routeRateLimitPart(request: FastifyRequest): string {
|
|
|
|
|
|
|
|
return `${request.method}:${request.routeOptions.url ?? request.url.split('?')[0]}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function emailRateLimitPart(request: FastifyRequest): string {
|
|
|
|
|
|
|
|
const body = request.body && typeof request.body === 'object' ? (request.body as Record<string, unknown>) : {};
|
|
|
|
|
|
|
|
const email = typeof body.email === 'string' ? body.email.trim().toLowerCase() : '';
|
|
|
|
|
|
|
|
return hashRateLimitPart(email || 'missing-email');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function userRateLimitPart(request: FastifyRequest): string {
|
|
|
|
|
|
|
|
return String((request as RateLimitedRequest).rateLimitUserId ?? 'anonymous');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function userRouteRateLimitKey(scope: string, request: FastifyRequest): string {
|
|
|
|
|
|
|
|
return `${scope}:user:${userRateLimitPart(request)}:route:${routeRateLimitPart(request)}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function ipRouteRateLimitKey(scope: string, request: FastifyRequest): string {
|
|
|
|
|
|
|
|
return `${scope}:ip:${hashRateLimitPart(request.ip)}:route:${routeRateLimitPart(request)}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const authRouteIpRateLimit = app.createRateLimit({
|
|
|
|
|
|
|
|
max: 20,
|
|
|
|
|
|
|
|
timeWindow: '10 minutes',
|
|
|
|
|
|
|
|
keyGenerator: (request) => ipRouteRateLimitKey('auth', request)
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const loginEmailRateLimit = app.createRateLimit({
|
|
|
|
|
|
|
|
max: 5,
|
|
|
|
|
|
|
|
timeWindow: '15 minutes',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `auth:login:email:${emailRateLimitPart(request)}`
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const registerEmailRateLimit = app.createRateLimit({
|
|
|
|
|
|
|
|
max: 3,
|
|
|
|
|
|
|
|
timeWindow: '1 hour',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `auth:register:email:${emailRateLimitPart(request)}`
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const passwordResetEmailRateLimit = app.createRateLimit({
|
|
|
|
|
|
|
|
max: 3,
|
|
|
|
|
|
|
|
timeWindow: '1 hour',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `auth:password-reset:email:${emailRateLimitPart(request)}`
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const passwordResetRouteIpRateLimit = app.createRateLimit({
|
|
|
|
|
|
|
|
max: 10,
|
|
|
|
|
|
|
|
timeWindow: '15 minutes',
|
|
|
|
|
|
|
|
keyGenerator: (request) => ipRouteRateLimitKey('auth:password-reset', request)
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const protectedRouteIpRateLimit = app.createRateLimit({
|
|
|
|
|
|
|
|
max: 120,
|
|
|
|
|
|
|
|
timeWindow: '10 minutes',
|
|
|
|
|
|
|
|
keyGenerator: (request) => ipRouteRateLimitKey('protected', request)
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const writeRouteIpRateLimit = app.createRateLimit({
|
|
|
|
|
|
|
|
max: 90,
|
|
|
|
|
|
|
|
timeWindow: '10 minutes',
|
|
|
|
|
|
|
|
keyGenerator: (request) => ipRouteRateLimitKey('write', request)
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const userRouteWriteRateLimit = app.createRateLimit({
|
|
|
|
|
|
|
|
max: 30,
|
|
|
|
|
|
|
|
timeWindow: '10 minutes',
|
|
|
|
|
|
|
|
keyGenerator: (request) => userRouteRateLimitKey('write', request)
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const fetchRouteIpRateLimit = app.createRateLimit({
|
|
|
|
|
|
|
|
max: 60,
|
|
|
|
|
|
|
|
timeWindow: '10 minutes',
|
|
|
|
|
|
|
|
keyGenerator: (request) => ipRouteRateLimitKey('fetch', request)
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const userRouteFetchRateLimit = app.createRateLimit({
|
|
|
|
|
|
|
|
max: 30,
|
|
|
|
|
|
|
|
timeWindow: '10 minutes',
|
|
|
|
|
|
|
|
keyGenerator: (request) => userRouteRateLimitKey('fetch', request)
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const userRateLimitPolicies: Record<RateLimitPolicy, RateLimitCheck[]> = {
|
|
|
|
|
|
|
|
accountWrite: [
|
|
|
|
|
|
|
|
writeRouteIpRateLimit,
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 20,
|
|
|
|
|
|
|
|
timeWindow: '1 hour',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `account-write:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 1,
|
|
|
|
|
|
|
|
timeWindow: '5 seconds',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `account-write-cooldown:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
userRouteWriteRateLimit
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
adminWrite: [
|
|
|
|
|
|
|
|
writeRouteIpRateLimit,
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 120,
|
|
|
|
|
|
|
|
timeWindow: '1 hour',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `admin-write:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 1,
|
|
|
|
|
|
|
|
timeWindow: '2 seconds',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `admin-write-cooldown:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
userRouteWriteRateLimit
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
communityReaction: [
|
|
|
|
|
|
|
|
writeRouteIpRateLimit,
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 120,
|
|
|
|
|
|
|
|
timeWindow: '1 hour',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `community-reaction:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 1,
|
|
|
|
|
|
|
|
timeWindow: '1 second',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `community-reaction-cooldown:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
userRouteWriteRateLimit
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
communityWrite: [
|
|
|
|
|
|
|
|
writeRouteIpRateLimit,
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 60,
|
|
|
|
|
|
|
|
timeWindow: '1 hour',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `community-write:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 1,
|
|
|
|
|
|
|
|
timeWindow: '5 seconds',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `community-write-cooldown:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
userRouteWriteRateLimit
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
fetch: [
|
|
|
|
|
|
|
|
fetchRouteIpRateLimit,
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 60,
|
|
|
|
|
|
|
|
timeWindow: '10 minutes',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `fetch:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 1,
|
|
|
|
|
|
|
|
timeWindow: '1 second',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `fetch-cooldown:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
userRouteFetchRateLimit
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
upload: [
|
|
|
|
|
|
|
|
writeRouteIpRateLimit,
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 20,
|
|
|
|
|
|
|
|
timeWindow: '1 hour',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `upload:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 1,
|
|
|
|
|
|
|
|
timeWindow: '30 seconds',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `upload-cooldown:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
userRouteWriteRateLimit
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
wikiWrite: [
|
|
|
|
|
|
|
|
writeRouteIpRateLimit,
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 120,
|
|
|
|
|
|
|
|
timeWindow: '1 hour',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `wiki-write:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
app.createRateLimit({
|
|
|
|
|
|
|
|
max: 1,
|
|
|
|
|
|
|
|
timeWindow: '2 seconds',
|
|
|
|
|
|
|
|
keyGenerator: (request) => `wiki-write-cooldown:user:${userRateLimitPart(request)}`
|
|
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
userRouteWriteRateLimit
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function sendRateLimited(
|
|
|
|
|
|
|
|
request: FastifyRequest,
|
|
|
|
|
|
|
|
reply: FastifyReply,
|
|
|
|
|
|
|
|
result: Extract<RateLimitResult, { isAllowed: false }>
|
|
|
|
|
|
|
|
): Promise<false> {
|
|
|
|
|
|
|
|
const retryAfter = Math.max(1, result.ttlInSeconds);
|
|
|
|
|
|
|
|
reply.header('retry-after', retryAfter);
|
|
|
|
|
|
|
|
reply.header('x-ratelimit-limit', result.max);
|
|
|
|
|
|
|
|
reply.header('x-ratelimit-remaining', 0);
|
|
|
|
|
|
|
|
reply.header('x-ratelimit-reset', retryAfter);
|
|
|
|
|
|
|
|
reply.code(result.isBanned ? 403 : 429).send({ message: await serverMessage(requestLocale(request), 'rateLimited') });
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function enforceRateLimits(
|
|
|
|
|
|
|
|
request: FastifyRequest,
|
|
|
|
|
|
|
|
reply: FastifyReply,
|
|
|
|
|
|
|
|
checks: RateLimitCheck[]
|
|
|
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
|
|
|
for (const check of checks) {
|
|
|
|
|
|
|
|
const result = await check(request);
|
|
|
|
|
|
|
|
if (!result.isAllowed && result.isExceeded) {
|
|
|
|
|
|
|
|
return sendRateLimited(request, reply, result);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function enforceAuthRateLimits(
|
|
|
|
|
|
|
|
request: FastifyRequest,
|
|
|
|
|
|
|
|
reply: FastifyReply,
|
|
|
|
|
|
|
|
checks: RateLimitCheck[]
|
|
|
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
|
|
|
return enforceRateLimits(request, reply, [authRouteIpRateLimit, ...checks]);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function enforceUserRateLimits(
|
|
|
|
|
|
|
|
request: FastifyRequest,
|
|
|
|
|
|
|
|
reply: FastifyReply,
|
|
|
|
|
|
|
|
user: AuthUser,
|
|
|
|
|
|
|
|
policy: RateLimitPolicy
|
|
|
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
|
|
|
(request as RateLimitedRequest).rateLimitUserId = user.id;
|
|
|
|
|
|
|
|
return enforceRateLimits(request, reply, userRateLimitPolicies[policy]);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function badRequest(message: string): Error & { statusCode: number } {
|
|
|
|
function badRequest(message: string): Error & { statusCode: number } {
|
|
|
|
const error = new Error(message) as Error & { statusCode: number };
|
|
|
|
const error = new Error(message) as Error & { statusCode: number };
|
|
|
|
error.statusCode = 400;
|
|
|
|
error.statusCode = 400;
|
|
|
|
@@ -197,6 +442,10 @@ async function notFound(reply: FastifyReply, request: FastifyRequest) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function requireVerifiedUser(request: FastifyRequest, reply: FastifyReply): Promise<AuthUser | null> {
|
|
|
|
async function requireVerifiedUser(request: FastifyRequest, reply: FastifyReply): Promise<AuthUser | null> {
|
|
|
|
|
|
|
|
if (!(await enforceRateLimits(request, reply, [protectedRouteIpRateLimit]))) {
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
const user = token ? await getUserBySessionToken(token) : null;
|
|
|
|
const user = token ? await getUserBySessionToken(token) : null;
|
|
|
|
const locale = requestLocale(request);
|
|
|
|
const locale = requestLocale(request);
|
|
|
|
@@ -250,6 +499,34 @@ async function requireAnyPermission(
|
|
|
|
return user;
|
|
|
|
return user;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function requirePermissionWithRateLimits(
|
|
|
|
|
|
|
|
request: FastifyRequest,
|
|
|
|
|
|
|
|
reply: FastifyReply,
|
|
|
|
|
|
|
|
permissionKey: string,
|
|
|
|
|
|
|
|
policy: RateLimitPolicy
|
|
|
|
|
|
|
|
): Promise<AuthUser | null> {
|
|
|
|
|
|
|
|
const user = await requirePermission(request, reply, permissionKey);
|
|
|
|
|
|
|
|
if (!user || !(await enforceUserRateLimits(request, reply, user, policy))) {
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return user;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function requireAnyPermissionWithRateLimits(
|
|
|
|
|
|
|
|
request: FastifyRequest,
|
|
|
|
|
|
|
|
reply: FastifyReply,
|
|
|
|
|
|
|
|
permissionKeys: string[],
|
|
|
|
|
|
|
|
policy: RateLimitPolicy
|
|
|
|
|
|
|
|
): Promise<AuthUser | null> {
|
|
|
|
|
|
|
|
const user = await requireAnyPermission(request, reply, permissionKeys);
|
|
|
|
|
|
|
|
if (!user || !(await enforceUserRateLimits(request, reply, user, policy))) {
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return user;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function optionalUser(request: FastifyRequest): Promise<AuthUser | null> {
|
|
|
|
async function optionalUser(request: FastifyRequest): Promise<AuthUser | null> {
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
if (!token) {
|
|
|
|
if (!token) {
|
|
|
|
@@ -263,23 +540,51 @@ async function optionalUser(request: FastifyRequest): Promise<AuthUser | null> {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/auth/register', async (request, reply) =>
|
|
|
|
app.post('/api/auth/register', async (request, reply) => {
|
|
|
|
reply.code(201).send(await registerUser(request.body as Record<string, unknown>, requestLocale(request)))
|
|
|
|
if (!(await enforceAuthRateLimits(request, reply, [registerEmailRateLimit]))) {
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/auth/verify-email', async (request) => verifyEmail(request.body as Record<string, unknown>, requestLocale(request)));
|
|
|
|
return reply.code(201).send(await registerUser(request.body as Record<string, unknown>, requestLocale(request)));
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/auth/login', async (request) => loginUser(request.body as Record<string, unknown>, requestLocale(request)));
|
|
|
|
app.post('/api/auth/verify-email', async (request, reply) => {
|
|
|
|
|
|
|
|
if (!(await enforceAuthRateLimits(request, reply, []))) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/auth/request-password-reset', async (request) =>
|
|
|
|
return verifyEmail(request.body as Record<string, unknown>, requestLocale(request));
|
|
|
|
requestPasswordReset(request.body as Record<string, unknown>, requestLocale(request))
|
|
|
|
});
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/auth/reset-password', async (request) =>
|
|
|
|
app.post('/api/auth/login', async (request, reply) => {
|
|
|
|
resetPassword(request.body as Record<string, unknown>, requestLocale(request))
|
|
|
|
if (!(await enforceAuthRateLimits(request, reply, [loginEmailRateLimit]))) {
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return loginUser(request.body as Record<string, unknown>, requestLocale(request));
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/auth/request-password-reset', async (request, reply) => {
|
|
|
|
|
|
|
|
if (!(await enforceAuthRateLimits(request, reply, [passwordResetEmailRateLimit, passwordResetRouteIpRateLimit]))) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return requestPasswordReset(request.body as Record<string, unknown>, requestLocale(request));
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/auth/reset-password', async (request, reply) => {
|
|
|
|
|
|
|
|
if (!(await enforceAuthRateLimits(request, reply, [passwordResetRouteIpRateLimit]))) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return resetPassword(request.body as Record<string, unknown>, requestLocale(request));
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.get('/api/auth/me', async (request, reply) => {
|
|
|
|
app.get('/api/auth/me', async (request, reply) => {
|
|
|
|
|
|
|
|
if (!(await enforceRateLimits(request, reply, [protectedRouteIpRateLimit]))) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
const user = token ? await getUserBySessionToken(token) : null;
|
|
|
|
const user = token ? await getUserBySessionToken(token) : null;
|
|
|
|
|
|
|
|
|
|
|
|
@@ -291,6 +596,10 @@ app.get('/api/auth/me', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.patch('/api/auth/me', async (request, reply) => {
|
|
|
|
app.patch('/api/auth/me', async (request, reply) => {
|
|
|
|
|
|
|
|
if (!(await enforceRateLimits(request, reply, [protectedRouteIpRateLimit]))) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
const user = token ? await getUserBySessionToken(token) : null;
|
|
|
|
const user = token ? await getUserBySessionToken(token) : null;
|
|
|
|
|
|
|
|
|
|
|
|
@@ -298,11 +607,19 @@ app.patch('/api/auth/me', async (request, reply) => {
|
|
|
|
return reply.code(401).send({ message: await serverMessage(requestLocale(request), 'loginRequired') });
|
|
|
|
return reply.code(401).send({ message: await serverMessage(requestLocale(request), 'loginRequired') });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!(await enforceUserRateLimits(request, reply, user, 'accountWrite'))) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const payload = request.body && typeof request.body === 'object' ? (request.body as Record<string, unknown>) : {};
|
|
|
|
const payload = request.body && typeof request.body === 'object' ? (request.body as Record<string, unknown>) : {};
|
|
|
|
return { user: await updateCurrentUser(user.id, payload, requestLocale(request)) };
|
|
|
|
return { user: await updateCurrentUser(user.id, payload, requestLocale(request)) };
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.patch('/api/auth/me/password', async (request, reply) => {
|
|
|
|
app.patch('/api/auth/me/password', async (request, reply) => {
|
|
|
|
|
|
|
|
if (!(await enforceRateLimits(request, reply, [protectedRouteIpRateLimit]))) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
const user = token ? await getUserBySessionToken(token) : null;
|
|
|
|
const user = token ? await getUserBySessionToken(token) : null;
|
|
|
|
|
|
|
|
|
|
|
|
@@ -310,11 +627,19 @@ app.patch('/api/auth/me/password', async (request, reply) => {
|
|
|
|
return reply.code(401).send({ message: await serverMessage(requestLocale(request), 'loginRequired') });
|
|
|
|
return reply.code(401).send({ message: await serverMessage(requestLocale(request), 'loginRequired') });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!(await enforceUserRateLimits(request, reply, user, 'accountWrite'))) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const payload = request.body && typeof request.body === 'object' ? (request.body as Record<string, unknown>) : {};
|
|
|
|
const payload = request.body && typeof request.body === 'object' ? (request.body as Record<string, unknown>) : {};
|
|
|
|
return changeCurrentUserPassword(user.id, payload, token, requestLocale(request));
|
|
|
|
return changeCurrentUserPassword(user.id, payload, token, requestLocale(request));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.get('/api/auth/referral', async (request, reply) => {
|
|
|
|
app.get('/api/auth/referral', async (request, reply) => {
|
|
|
|
|
|
|
|
if (!(await enforceRateLimits(request, reply, [protectedRouteIpRateLimit]))) {
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
const token = getBearerToken(request.headers.authorization);
|
|
|
|
const user = token ? await getUserBySessionToken(token) : null;
|
|
|
|
const user = token ? await getUserBySessionToken(token) : null;
|
|
|
|
|
|
|
|
|
|
|
|
@@ -340,7 +665,7 @@ app.get('/api/admin/users', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/users/:id/roles', async (request, reply) => {
|
|
|
|
app.put('/api/admin/users/:id/roles', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.users.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.users.update', 'adminWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -354,12 +679,12 @@ app.get('/api/admin/roles', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/admin/roles', async (request, reply) => {
|
|
|
|
app.post('/api/admin/roles', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.roles.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.roles.create', 'adminWrite');
|
|
|
|
return user ? reply.code(201).send(await createRole(request.body as Record<string, unknown>)) : undefined;
|
|
|
|
return user ? reply.code(201).send(await createRole(request.body as Record<string, unknown>)) : undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/roles/:id', async (request, reply) => {
|
|
|
|
app.put('/api/admin/roles/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.roles.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.roles.update', 'adminWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -368,7 +693,7 @@ app.put('/api/admin/roles/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/roles/:id/permissions', async (request, reply) => {
|
|
|
|
app.put('/api/admin/roles/:id/permissions', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.roles.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.roles.update', 'adminWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -377,7 +702,7 @@ app.put('/api/admin/roles/:id/permissions', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/admin/roles/:id', async (request, reply) => {
|
|
|
|
app.delete('/api/admin/roles/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.roles.delete');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.roles.delete', 'adminWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -392,12 +717,12 @@ app.get('/api/admin/permissions', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/admin/permissions', async (request, reply) => {
|
|
|
|
app.post('/api/admin/permissions', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.permissions.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.permissions.create', 'adminWrite');
|
|
|
|
return user ? reply.code(201).send(await createPermission(request.body as Record<string, unknown>)) : undefined;
|
|
|
|
return user ? reply.code(201).send(await createPermission(request.body as Record<string, unknown>)) : undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/permissions/:id', async (request, reply) => {
|
|
|
|
app.put('/api/admin/permissions/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.permissions.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.permissions.update', 'adminWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -406,7 +731,7 @@ app.put('/api/admin/permissions/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/admin/permissions/:id', async (request, reply) => {
|
|
|
|
app.delete('/api/admin/permissions/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.permissions.delete');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.permissions.delete', 'adminWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -469,14 +794,14 @@ app.get('/api/life-posts', async (request) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/life-posts', async (request, reply) => {
|
|
|
|
app.post('/api/life-posts', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'life.posts.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'life.posts.create', 'communityWrite');
|
|
|
|
return user
|
|
|
|
return user
|
|
|
|
? reply.code(201).send(await createLifePost(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
|
|
|
? reply.code(201).send(await createLifePost(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
|
|
|
: undefined;
|
|
|
|
: undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/life-posts/:postId/comments', async (request, reply) => {
|
|
|
|
app.post('/api/life-posts/:postId/comments', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'life.comments.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'life.comments.create', 'communityWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -486,7 +811,7 @@ app.post('/api/life-posts/:postId/comments', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/life-posts/:postId/comments/:commentId/replies', async (request, reply) => {
|
|
|
|
app.post('/api/life-posts/:postId/comments/:commentId/replies', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'life.comments.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'life.comments.create', 'communityWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -501,7 +826,12 @@ app.post('/api/life-posts/:postId/comments/:commentId/replies', async (request,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/life-posts/:id', async (request, reply) => {
|
|
|
|
app.put('/api/life-posts/:id', async (request, reply) => {
|
|
|
|
const user = await requireAnyPermission(request, reply, ['life.posts.update', 'life.posts.update-any']);
|
|
|
|
const user = await requireAnyPermissionWithRateLimits(
|
|
|
|
|
|
|
|
request,
|
|
|
|
|
|
|
|
reply,
|
|
|
|
|
|
|
|
['life.posts.update', 'life.posts.update-any'],
|
|
|
|
|
|
|
|
'communityWrite'
|
|
|
|
|
|
|
|
);
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -517,7 +847,7 @@ app.put('/api/life-posts/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/life-posts/:id/reaction', async (request, reply) => {
|
|
|
|
app.put('/api/life-posts/:id/reaction', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'life.reactions.set');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'life.reactions.set', 'communityReaction');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -527,7 +857,7 @@ app.put('/api/life-posts/:id/reaction', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/life-posts/:id/reaction', async (request, reply) => {
|
|
|
|
app.delete('/api/life-posts/:id/reaction', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'life.reactions.set');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'life.reactions.set', 'communityReaction');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -537,7 +867,12 @@ app.delete('/api/life-posts/:id/reaction', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/life-posts/:id', async (request, reply) => {
|
|
|
|
app.delete('/api/life-posts/:id', async (request, reply) => {
|
|
|
|
const user = await requireAnyPermission(request, reply, ['life.posts.delete', 'life.posts.delete-any']);
|
|
|
|
const user = await requireAnyPermissionWithRateLimits(
|
|
|
|
|
|
|
|
request,
|
|
|
|
|
|
|
|
reply,
|
|
|
|
|
|
|
|
['life.posts.delete', 'life.posts.delete-any'],
|
|
|
|
|
|
|
|
'communityWrite'
|
|
|
|
|
|
|
|
);
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -547,7 +882,12 @@ app.delete('/api/life-posts/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/life-comments/:id', async (request, reply) => {
|
|
|
|
app.delete('/api/life-comments/:id', async (request, reply) => {
|
|
|
|
const user = await requireAnyPermission(request, reply, ['life.comments.delete', 'life.comments.delete-any']);
|
|
|
|
const user = await requireAnyPermissionWithRateLimits(
|
|
|
|
|
|
|
|
request,
|
|
|
|
|
|
|
|
reply,
|
|
|
|
|
|
|
|
['life.comments.delete', 'life.comments.delete-any'],
|
|
|
|
|
|
|
|
'communityWrite'
|
|
|
|
|
|
|
|
);
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -563,7 +903,7 @@ app.get('/api/discussions/:entityType/:entityId/comments', async (request, reply
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/discussions/:entityType/:entityId/comments', async (request, reply) => {
|
|
|
|
app.post('/api/discussions/:entityType/:entityId/comments', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'discussions.comments.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'discussions.comments.create', 'communityWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -579,7 +919,7 @@ app.post('/api/discussions/:entityType/:entityId/comments', async (request, repl
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/discussions/:entityType/:entityId/comments/:commentId/replies', async (request, reply) => {
|
|
|
|
app.post('/api/discussions/:entityType/:entityId/comments/:commentId/replies', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'discussions.comments.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'discussions.comments.create', 'communityWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -600,10 +940,12 @@ app.post('/api/discussions/:entityType/:entityId/comments/:commentId/replies', a
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/discussions/comments/:id', async (request, reply) => {
|
|
|
|
app.delete('/api/discussions/comments/:id', async (request, reply) => {
|
|
|
|
const user = await requireAnyPermission(request, reply, [
|
|
|
|
const user = await requireAnyPermissionWithRateLimits(
|
|
|
|
'discussions.comments.delete',
|
|
|
|
request,
|
|
|
|
'discussions.comments.delete-any'
|
|
|
|
reply,
|
|
|
|
]);
|
|
|
|
['discussions.comments.delete', 'discussions.comments.delete-any'],
|
|
|
|
|
|
|
|
'communityWrite'
|
|
|
|
|
|
|
|
);
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -622,7 +964,7 @@ app.get('/api/pokemon', async (request) =>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
app.get('/api/pokemon/fetch-options', async (request, reply) => {
|
|
|
|
app.get('/api/pokemon/fetch-options', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'pokemon.fetch');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'pokemon.fetch', 'fetch');
|
|
|
|
return user
|
|
|
|
return user
|
|
|
|
? listPokemonFetchOptions(request.query as Record<string, string | string[] | undefined>, requestLocale(request))
|
|
|
|
? listPokemonFetchOptions(request.query as Record<string, string | string[] | undefined>, requestLocale(request))
|
|
|
|
: undefined;
|
|
|
|
: undefined;
|
|
|
|
@@ -640,19 +982,19 @@ app.get('/api/pokemon/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/pokemon', async (request, reply) => {
|
|
|
|
app.post('/api/pokemon', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'pokemon.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'pokemon.create', 'wikiWrite');
|
|
|
|
return user
|
|
|
|
return user
|
|
|
|
? reply.code(201).send(await createPokemon(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
|
|
|
? reply.code(201).send(await createPokemon(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
|
|
|
: undefined;
|
|
|
|
: undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/pokemon/fetch', async (request, reply) => {
|
|
|
|
app.post('/api/pokemon/fetch', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'pokemon.fetch');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'pokemon.fetch', 'fetch');
|
|
|
|
return user ? fetchPokemonData(request.body as Record<string, unknown>, user.id) : undefined;
|
|
|
|
return user ? fetchPokemonData(request.body as Record<string, unknown>, user.id) : undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/pokemon/image-options', async (request, reply) => {
|
|
|
|
app.post('/api/pokemon/image-options', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'pokemon.fetch');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'pokemon.fetch', 'fetch');
|
|
|
|
return user ? fetchPokemonImageOptions(request.body as Record<string, unknown>) : undefined;
|
|
|
|
return user ? fetchPokemonImageOptions(request.body as Record<string, unknown>) : undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
@@ -664,7 +1006,7 @@ app.post('/api/uploads/:entityType', async (request, reply) => {
|
|
|
|
|
|
|
|
|
|
|
|
const permissionKey =
|
|
|
|
const permissionKey =
|
|
|
|
entityType === 'pokemon' ? 'pokemon.upload' : entityType === 'items' ? 'items.upload' : 'habitats.upload';
|
|
|
|
entityType === 'pokemon' ? 'pokemon.upload' : entityType === 'items' ? 'items.upload' : 'habitats.upload';
|
|
|
|
const user = await requirePermission(request, reply, permissionKey);
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, permissionKey, 'upload');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -684,7 +1026,7 @@ app.post('/api/uploads/:entityType', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/pokemon/:id', async (request, reply) => {
|
|
|
|
app.put('/api/pokemon/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'pokemon.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'pokemon.update', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -699,7 +1041,7 @@ app.put('/api/pokemon/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/pokemon/:id', async (request, reply) => {
|
|
|
|
app.delete('/api/pokemon/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'pokemon.delete');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'pokemon.delete', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -722,14 +1064,14 @@ app.get('/api/habitats/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/habitats', async (request, reply) => {
|
|
|
|
app.post('/api/habitats', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'habitats.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'habitats.create', 'wikiWrite');
|
|
|
|
return user
|
|
|
|
return user
|
|
|
|
? reply.code(201).send(await createHabitat(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
|
|
|
? reply.code(201).send(await createHabitat(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
|
|
|
: undefined;
|
|
|
|
: undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/habitats/:id', async (request, reply) => {
|
|
|
|
app.put('/api/habitats/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'habitats.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'habitats.update', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -744,7 +1086,7 @@ app.put('/api/habitats/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/habitats/:id', async (request, reply) => {
|
|
|
|
app.delete('/api/habitats/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'habitats.delete');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'habitats.delete', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -769,14 +1111,14 @@ app.get('/api/items/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/items', async (request, reply) => {
|
|
|
|
app.post('/api/items', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'items.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'items.create', 'wikiWrite');
|
|
|
|
return user
|
|
|
|
return user
|
|
|
|
? reply.code(201).send(await createItem(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
|
|
|
? reply.code(201).send(await createItem(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
|
|
|
: undefined;
|
|
|
|
: undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/items/:id', async (request, reply) => {
|
|
|
|
app.put('/api/items/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'items.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'items.update', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -791,7 +1133,7 @@ app.put('/api/items/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/items/:id', async (request, reply) => {
|
|
|
|
app.delete('/api/items/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'items.delete');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'items.delete', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -816,14 +1158,14 @@ app.get('/api/recipes/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/recipes', async (request, reply) => {
|
|
|
|
app.post('/api/recipes', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'recipes.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'recipes.create', 'wikiWrite');
|
|
|
|
return user
|
|
|
|
return user
|
|
|
|
? reply.code(201).send(await createRecipe(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
|
|
|
? reply.code(201).send(await createRecipe(request.body as Record<string, unknown>, user.id, requestLocale(request)))
|
|
|
|
: undefined;
|
|
|
|
: undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/recipes/:id', async (request, reply) => {
|
|
|
|
app.put('/api/recipes/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'recipes.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'recipes.update', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -838,7 +1180,7 @@ app.put('/api/recipes/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/recipes/:id', async (request, reply) => {
|
|
|
|
app.delete('/api/recipes/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'recipes.delete');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'recipes.delete', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -848,7 +1190,7 @@ app.delete('/api/recipes/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/admin/daily-checklist', async (request, reply) => {
|
|
|
|
app.post('/api/admin/daily-checklist', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'checklist.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'checklist.create', 'wikiWrite');
|
|
|
|
return user
|
|
|
|
return user
|
|
|
|
? reply
|
|
|
|
? reply
|
|
|
|
.code(201)
|
|
|
|
.code(201)
|
|
|
|
@@ -857,12 +1199,12 @@ app.post('/api/admin/daily-checklist', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/daily-checklist/order', async (request, reply) => {
|
|
|
|
app.put('/api/admin/daily-checklist/order', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'checklist.order');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'checklist.order', 'wikiWrite');
|
|
|
|
return user ? reorderDailyChecklistItems(request.body as Record<string, unknown>, user.id, requestLocale(request)) : 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) => {
|
|
|
|
app.put('/api/admin/daily-checklist/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'checklist.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'checklist.update', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -877,7 +1219,7 @@ app.put('/api/admin/daily-checklist/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/admin/daily-checklist/:id', async (request, reply) => {
|
|
|
|
app.delete('/api/admin/daily-checklist/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'checklist.delete');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'checklist.delete', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -887,22 +1229,22 @@ app.delete('/api/admin/daily-checklist/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/pokemon/order', async (request, reply) => {
|
|
|
|
app.put('/api/admin/pokemon/order', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'pokemon.order');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'pokemon.order', 'wikiWrite');
|
|
|
|
return user ? reorderPokemon(request.body as Record<string, unknown>, user.id, requestLocale(request)) : undefined;
|
|
|
|
return user ? reorderPokemon(request.body as Record<string, unknown>, user.id, requestLocale(request)) : undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/items/order', async (request, reply) => {
|
|
|
|
app.put('/api/admin/items/order', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'items.order');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'items.order', 'wikiWrite');
|
|
|
|
return user ? reorderItems(request.body as Record<string, unknown>, user.id, requestLocale(request)) : undefined;
|
|
|
|
return user ? reorderItems(request.body as Record<string, unknown>, user.id, requestLocale(request)) : undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/recipes/order', async (request, reply) => {
|
|
|
|
app.put('/api/admin/recipes/order', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'recipes.order');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'recipes.order', 'wikiWrite');
|
|
|
|
return user ? reorderRecipes(request.body as Record<string, unknown>, user.id, requestLocale(request)) : undefined;
|
|
|
|
return user ? reorderRecipes(request.body as Record<string, unknown>, user.id, requestLocale(request)) : undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/habitats/order', async (request, reply) => {
|
|
|
|
app.put('/api/admin/habitats/order', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'habitats.order');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'habitats.order', 'wikiWrite');
|
|
|
|
return user ? reorderHabitats(request.body as Record<string, unknown>, user.id, requestLocale(request)) : undefined;
|
|
|
|
return user ? reorderHabitats(request.body as Record<string, unknown>, user.id, requestLocale(request)) : undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
@@ -912,17 +1254,17 @@ app.get('/api/admin/languages', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/admin/languages', async (request, reply) => {
|
|
|
|
app.post('/api/admin/languages', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.languages.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.languages.create', 'adminWrite');
|
|
|
|
return user ? reply.code(201).send(await createLanguage(request.body as Record<string, unknown>)) : undefined;
|
|
|
|
return user ? reply.code(201).send(await createLanguage(request.body as Record<string, unknown>)) : undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/languages/order', async (request, reply) => {
|
|
|
|
app.put('/api/admin/languages/order', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.languages.order');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.languages.order', 'adminWrite');
|
|
|
|
return user ? reorderLanguages(request.body as Record<string, unknown>) : undefined;
|
|
|
|
return user ? reorderLanguages(request.body as Record<string, unknown>) : undefined;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/languages/:code', async (request, reply) => {
|
|
|
|
app.put('/api/admin/languages/:code', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.languages.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.languages.update', 'adminWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -931,7 +1273,7 @@ app.put('/api/admin/languages/:code', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/admin/languages/:code', async (request, reply) => {
|
|
|
|
app.delete('/api/admin/languages/:code', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.languages.delete');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.languages.delete', 'adminWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -946,7 +1288,7 @@ app.get('/api/admin/system-wordings', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/system-wordings/:key', async (request, reply) => {
|
|
|
|
app.put('/api/admin/system-wordings/:key', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.wordings.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.wordings.update', 'adminWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -967,7 +1309,7 @@ app.get('/api/admin/config/:type', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.post('/api/admin/config/:type', async (request, reply) => {
|
|
|
|
app.post('/api/admin/config/:type', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.config.create');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.config.create', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -981,7 +1323,7 @@ app.post('/api/admin/config/:type', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/config/:type/order', async (request, reply) => {
|
|
|
|
app.put('/api/admin/config/:type/order', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.config.order');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.config.order', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -993,7 +1335,7 @@ app.put('/api/admin/config/:type/order', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.put('/api/admin/config/:type/:id', async (request, reply) => {
|
|
|
|
app.put('/api/admin/config/:type/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.config.update');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.config.update', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@@ -1006,7 +1348,7 @@ app.put('/api/admin/config/:type/:id', async (request, reply) => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
app.delete('/api/admin/config/:type/:id', async (request, reply) => {
|
|
|
|
app.delete('/api/admin/config/:type/:id', async (request, reply) => {
|
|
|
|
const user = await requirePermission(request, reply, 'admin.config.delete');
|
|
|
|
const user = await requirePermissionWithRateLimits(request, reply, 'admin.config.delete', 'wikiWrite');
|
|
|
|
if (!user) {
|
|
|
|
if (!user) {
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|