diff --git a/CHANGELOG.md b/CHANGELOG.md index edfe09391..f16df03ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ You should also include the user name that made the change. ## 12.x.x (unreleased) ### Improvements +- Server: Allow GET method for some endpoints @syuilo - Server: Add rate limit to i/notifications @tamaina - Client: Improve control panel @syuilo - Client: Show warning in control panel when there is an unresolved abuse report @syuilo diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts index f97c3dd39..d2d16597e 100644 --- a/packages/backend/src/server/api/api-handler.ts +++ b/packages/backend/src/server/api/api-handler.ts @@ -6,7 +6,11 @@ import call from './call.js'; import { ApiError } from './error.js'; export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => { - const body = ctx.request.body; + const body = ctx.is('multipart/form-data') + ? (ctx.req as any).body + : ctx.method === 'GET' + ? ctx.query + : ctx.request.body; const reply = (x?: any, y?: ApiError) => { if (x == null) { @@ -33,6 +37,9 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res authenticate(body['i']).then(([user, app]) => { // API invoking call(endpoint.name, user, app, body, ctx).then((res: any) => { + if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) { + ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`); + } reply(res); }).catch((e: ApiError) => { reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index edd561ed8..fe1698dbc 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -94,7 +94,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi } // Cast non JSON input - if (ep.meta.requireFile && ep.params.properties) { + if ((ep.meta.requireFile || ctx?.method === 'GET') && ep.params.properties) { for (const k of Object.keys(ep.params.properties)) { const param = ep.params.properties![k]; if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') { diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 5fac7df23..11d9d7c02 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -701,6 +701,16 @@ export interface IEndpointMeta { readonly kind?: string; readonly description?: string; + + /** + * GETでのリクエストを許容するか否か + */ + readonly allowGet?: boolean; + + /** + * 正常応答をキャッシュ (Cache-Control: public) する秒数 + */ + readonly cacheSec?: number; } export interface IEndpoint { diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts index 97f7885db..ea2379429 100644 --- a/packages/backend/src/server/api/endpoints/charts/active-users.ts +++ b/packages/backend/src/server/api/endpoints/charts/active-users.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { activeUsersChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts', 'users'], res: getJsonSchema(activeUsersChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/ap-request.ts b/packages/backend/src/server/api/endpoints/charts/ap-request.ts index 4477bfc98..06dee250e 100644 --- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts +++ b/packages/backend/src/server/api/endpoints/charts/ap-request.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { apRequestChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts'], res: getJsonSchema(apRequestChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts index fd6033392..dd2c2d683 100644 --- a/packages/backend/src/server/api/endpoints/charts/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/drive.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { driveChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts', 'drive'], res: getJsonSchema(driveChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts index f842f574e..8c35b3c46 100644 --- a/packages/backend/src/server/api/endpoints/charts/federation.ts +++ b/packages/backend/src/server/api/endpoints/charts/federation.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { federationChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts'], res: getJsonSchema(federationChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/hashtag.ts b/packages/backend/src/server/api/endpoints/charts/hashtag.ts index 01407defd..77e24a62c 100644 --- a/packages/backend/src/server/api/endpoints/charts/hashtag.ts +++ b/packages/backend/src/server/api/endpoints/charts/hashtag.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { hashtagChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts', 'hashtags'], res: getJsonSchema(hashtagChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts index 2d12951c6..817d51ad0 100644 --- a/packages/backend/src/server/api/endpoints/charts/instance.ts +++ b/packages/backend/src/server/api/endpoints/charts/instance.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { instanceChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts'], res: getJsonSchema(instanceChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts index b6089f67e..951adf540 100644 --- a/packages/backend/src/server/api/endpoints/charts/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/notes.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { notesChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts', 'notes'], res: getJsonSchema(notesChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts index e5db7131a..f165b4022 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/drive.ts @@ -1,11 +1,14 @@ -import define from '../../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { perUserDriveChart } from '@/services/chart/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['charts', 'drive', 'users'], res: getJsonSchema(perUserDriveChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts index 9b72de745..f5d42e21c 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/following.ts @@ -6,6 +6,9 @@ export const meta = { tags: ['charts', 'users', 'following'], res: getJsonSchema(perUserFollowingChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts index 7cc6cbf31..aefe550d4 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/notes.ts @@ -1,11 +1,14 @@ -import define from '../../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { perUserNotesChart } from '@/services/chart/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['charts', 'users', 'notes'], res: getJsonSchema(perUserNotesChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts index 5c58a7f15..6bc6b56bf 100644 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts @@ -1,11 +1,14 @@ -import define from '../../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { perUserReactionsChart } from '@/services/chart/index.js'; +import define from '../../../define.js'; export const meta = { tags: ['charts', 'users', 'reactions'], res: getJsonSchema(perUserReactionsChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts index 49c762b2e..338e8fd33 100644 --- a/packages/backend/src/server/api/endpoints/charts/users.ts +++ b/packages/backend/src/server/api/endpoints/charts/users.ts @@ -1,11 +1,14 @@ -import define from '../../define.js'; import { getJsonSchema } from '@/services/chart/core.js'; import { usersChart } from '@/services/chart/index.js'; +import define from '../../define.js'; export const meta = { tags: ['charts', 'users'], res: getJsonSchema(usersChart.schema), + + allowGet: true, + cacheSec: 60 * 60, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts index 02bec31b1..83ece51f5 100644 --- a/packages/backend/src/server/api/index.ts +++ b/packages/backend/src/server/api/index.ts @@ -8,6 +8,8 @@ import multer from '@koa/multer'; import bodyParser from 'koa-bodyparser'; import cors from '@koa/cors'; +import { Instances, AccessTokens, Users } from '@/models/index.js'; +import config from '@/config/index.js'; import endpoints from './endpoints.js'; import handler from './api-handler.js'; import signup from './private/signup.js'; @@ -16,8 +18,6 @@ import signupPending from './private/signup-pending.js'; import discord from './service/discord.js'; import github from './service/github.js'; import twitter from './service/twitter.js'; -import { Instances, AccessTokens, Users } from '@/models/index.js'; -import config from '@/config/index.js'; // Init app const app = new Koa(); @@ -56,11 +56,24 @@ for (const endpoint of endpoints) { if (endpoint.meta.requireFile) { router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint)); } else { + // 後方互換性のため if (endpoint.name.includes('-')) { - // 後方互換性のため router.post(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint)); + + if (endpoint.meta.allowGet) { + router.get(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint)); + } else { + router.get(`/${endpoint.name.replace(/-/g, '_')}`, async ctx => { ctx.status = 405; }); + } } + router.post(`/${endpoint.name}`, handler.bind(null, endpoint)); + + if (endpoint.meta.allowGet) { + router.get(`/${endpoint.name}`, handler.bind(null, endpoint)); + } else { + router.get(`/${endpoint.name}`, async ctx => { ctx.status = 405; }); + } } } diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index d9075dd07..fc7c4ff95 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -342,7 +342,7 @@ const exportData = () => { }; const fetchFederationChart = async (): Promise => { - const raw = await os.api('charts/federation', { limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/federation', { limit: props.limit, span: props.span }); return { series: [{ name: 'Received', @@ -392,7 +392,7 @@ const fetchFederationChart = async (): Promise => { }; const fetchApRequestChart = async (): Promise => { - const raw = await os.api('charts/ap-request', { limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/ap-request', { limit: props.limit, span: props.span }); return { series: [{ name: 'In', @@ -414,7 +414,7 @@ const fetchApRequestChart = async (): Promise => { }; const fetchNotesChart = async (type: string): Promise => { - const raw = await os.api('charts/notes', { limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/notes', { limit: props.limit, span: props.span }); return { series: [{ name: 'All', @@ -461,7 +461,7 @@ const fetchNotesChart = async (type: string): Promise => { }; const fetchNotesTotalChart = async (): Promise => { - const raw = await os.api('charts/notes', { limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/notes', { limit: props.limit, span: props.span }); return { series: [{ name: 'Combined', @@ -480,7 +480,7 @@ const fetchNotesTotalChart = async (): Promise => { }; const fetchUsersChart = async (total: boolean): Promise => { - const raw = await os.api('charts/users', { limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/users', { limit: props.limit, span: props.span }); return { series: [{ name: 'Combined', @@ -508,7 +508,7 @@ const fetchUsersChart = async (total: boolean): Promise => { }; const fetchActiveUsersChart = async (): Promise => { - const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/active-users', { limit: props.limit, span: props.span }); return { series: [{ name: 'Read & Write', @@ -560,7 +560,7 @@ const fetchActiveUsersChart = async (): Promise => { }; const fetchDriveChart = async (): Promise => { - const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/drive', { limit: props.limit, span: props.span }); return { bytes: true, series: [{ @@ -596,7 +596,7 @@ const fetchDriveChart = async (): Promise => { }; const fetchDriveFilesChart = async (): Promise => { - const raw = await os.api('charts/drive', { limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/drive', { limit: props.limit, span: props.span }); return { series: [{ name: 'All', @@ -631,7 +631,7 @@ const fetchDriveFilesChart = async (): Promise => { }; const fetchInstanceRequestsChart = async (): Promise => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { series: [{ name: 'In', @@ -653,7 +653,7 @@ const fetchInstanceRequestsChart = async (): Promise => { }; const fetchInstanceUsersChart = async (total: boolean): Promise => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { series: [{ name: 'Users', @@ -668,7 +668,7 @@ const fetchInstanceUsersChart = async (total: boolean): Promise => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { series: [{ name: 'Notes', @@ -683,7 +683,7 @@ const fetchInstanceNotesChart = async (total: boolean): Promise => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { series: [{ name: 'Following', @@ -706,7 +706,7 @@ const fetchInstanceFfChart = async (total: boolean): Promise = }; const fetchInstanceDriveUsageChart = async (total: boolean): Promise => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { bytes: true, series: [{ @@ -722,7 +722,7 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise => { - const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span }); return { series: [{ name: 'Drive files', @@ -737,7 +737,7 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise => { - const raw = await os.api('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span }); return { series: [...(props.args.withoutAll ? [] : [{ name: 'All', @@ -769,7 +769,7 @@ const fetchPerUserNotesChart = async (): Promise => { }; const fetchPerUserFollowingChart = async (): Promise => { - const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); return { series: [{ name: 'Local', @@ -784,7 +784,7 @@ const fetchPerUserFollowingChart = async (): Promise => { }; const fetchPerUserFollowersChart = async (): Promise => { - const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span }); return { series: [{ name: 'Local', @@ -799,7 +799,7 @@ const fetchPerUserFollowersChart = async (): Promise => { }; const fetchPerUserDriveChart = async (): Promise => { - const raw = await os.api('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span }); + const raw = await os.apiGet('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span }); return { series: [{ name: 'Inc', diff --git a/packages/client/src/components/instance-card-mini.vue b/packages/client/src/components/instance-card-mini.vue index 0bac6c0ad..de16dacf7 100644 --- a/packages/client/src/components/instance-card-mini.vue +++ b/packages/client/src/components/instance-card-mini.vue @@ -20,7 +20,7 @@ const props = defineProps<{ const chart = $ref(null); -os.api('charts/instance', { host: props.instance.host, limit: 16, span: 'day' }).then(res => { +os.apiGet('charts/instance', { host: props.instance.host, limit: 16, span: 'day' }).then(res => { chart = res; }); diff --git a/packages/client/src/components/user-card-mini.vue b/packages/client/src/components/user-card-mini.vue index d83e4a56f..2c3e0fe95 100644 --- a/packages/client/src/components/user-card-mini.vue +++ b/packages/client/src/components/user-card-mini.vue @@ -21,7 +21,7 @@ const props = defineProps<{ const chart = $ref(null); -os.api('charts/user/notes', { userId: props.user.id, limit: 16, span: 'day' }).then(res => { +os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16, span: 'day' }).then(res => { chart = res; }); diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index e823d3719..00dae867d 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -52,6 +52,39 @@ export const api = ((endpoint: string, data: Record = {}, token?: s return promise; }) as typeof apiClient.request; +export const apiGet = ((endpoint: string, data: Record = {}) => { + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const query = new URLSearchParams(data); + + const promise = new Promise((resolve, reject) => { + // Send request + fetch(`${apiUrl}/${endpoint}?${query}`, { + method: 'GET', + credentials: 'omit', + cache: 'default', + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +}) as typeof apiClient.request; + export const apiWithDialog = (( endpoint: string, data: Record = {}, diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue index 82b3c3385..f81f6104c 100644 --- a/packages/client/src/pages/admin/overview.vue +++ b/packages/client/src/pages/admin/overview.vue @@ -90,11 +90,11 @@ onMounted(async () => { os.api('stats', {}).then(statsResponse => { stats = statsResponse; - os.api('charts/users', { limit: 2, span: 'day' }).then(chart => { + os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => { usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1]; }); - os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => { + os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => { notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1]; }); }); diff --git a/packages/client/src/widgets/activity.vue b/packages/client/src/widgets/activity.vue index 7fb9f5894..265bde4a3 100644 --- a/packages/client/src/widgets/activity.vue +++ b/packages/client/src/widgets/activity.vue @@ -15,12 +15,12 @@