forked from FoundKeyGang/FoundKey
Compare commits
5 commits
main
...
secure-mod
Author | SHA1 | Date | |
---|---|---|---|
aa76c974f3 | |||
61b7c8ca53 | |||
840227a901 | |||
9acd4bc855 | |||
8bd41f5c9e |
85 changed files with 532 additions and 30 deletions
|
@ -829,6 +829,13 @@ middle: "Medium"
|
|||
low: "Low"
|
||||
emailNotConfiguredWarning: "Email address not set."
|
||||
ratio: "Ratio"
|
||||
secureMode: "Secure Mode (Authorized Fetch)"
|
||||
instanceSecurity: "Instance Security"
|
||||
secureModeInfo: "Requests from other instances must be signed, otherwise notes won't be returned."
|
||||
privateMode: "Private Mode"
|
||||
privateModeInfo: "When enabled, only authorized instances may fetch notes. Hides all notes from public."
|
||||
allowedInstances: "Allowed Instances"
|
||||
allowedInstancesDescription: "Set the hosts of the instances you want to allow, separated by line. Valid in private mode only."
|
||||
previewNoteText: "Show preview"
|
||||
customCss: "Custom CSS"
|
||||
customCssWarn: "This setting should only be used if you know what it does. Entering\
|
||||
|
|
|
@ -771,6 +771,13 @@ middle: "中"
|
|||
low: "低"
|
||||
emailNotConfiguredWarning: "メールアドレスの設定がされていません。"
|
||||
ratio: "比率"
|
||||
secureMode: "セキュアモード (Authorized Fetch)"
|
||||
instanceSecurity: "インスタンスのセキュリティー"
|
||||
secureModeInfo: "他のインスタンスからリクエストするときに、証明を付けなければ返送しません。"
|
||||
privateMode: "非公開モード"
|
||||
privateModeInfo: "有効にして、許可されているインスタンスのみがリクエストできます。すべてのノートが公開に非表示にします。"
|
||||
allowedInstances: "許可されたインスタンス"
|
||||
allowedInstancesDescription: "許可したいインスタンスのホストを改行で区切って設定します。非公開モードだけで有効です。"
|
||||
previewNoteText: "本文をプレビュー"
|
||||
customCss: "カスタムCSS"
|
||||
customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。"
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
export class allowlistSecureMode1626733991004 {
|
||||
name = 'allowlistSecureMode1626733991004';
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "allowedHosts" character varying(256) [] default '{}'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "secureMode" bool default false`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "privateMode" bool default false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowedHosts"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "secureMode"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "privateMode"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -77,6 +77,21 @@ export class Meta {
|
|||
})
|
||||
public blockedHosts: string[];
|
||||
|
||||
@Column('boolean', {
|
||||
default: false
|
||||
})
|
||||
public secureMode: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false
|
||||
})
|
||||
public privateMode: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, array: true, default: '{}'
|
||||
})
|
||||
public allowedHosts: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-foundkey}',
|
||||
})
|
||||
|
|
|
@ -28,6 +28,10 @@ export default async (job: Bull.Job<DeliverJobData>) => {
|
|||
return 'skip (blocked)';
|
||||
}
|
||||
|
||||
if (meta.privateMode && !meta.allowedHosts.includes(toPuny(host))) {
|
||||
return 'skip (not allowed)';
|
||||
}
|
||||
|
||||
// isSuspendedなら中断
|
||||
let suspendedHosts = suspendedHostsCache.get(null);
|
||||
if (suspendedHosts == null) {
|
||||
|
|
|
@ -39,6 +39,11 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
return `Blocked request: ${host}`;
|
||||
}
|
||||
|
||||
// Only permitted instances if in private mode.
|
||||
if (meta.privateMode && !meta.allowedHosts.includes(host)) {
|
||||
return `Blocked request: ${host}`;
|
||||
}
|
||||
|
||||
const keyIdLower = signature.keyId.toLowerCase();
|
||||
if (keyIdLower.startsWith('acct:')) {
|
||||
return `Old keyId is no longer supported. ${keyIdLower}`;
|
||||
|
|
69
packages/backend/src/remote/activitypub/check-fetch.ts
Normal file
69
packages/backend/src/remote/activitypub/check-fetch.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import config from '@/config/index.js';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
import { URL } from 'url';
|
||||
import { toPuny } from '@/misc/convert-host.js';
|
||||
import DbResolver from '@/remote/activitypub/db-resolver.js';
|
||||
import { getApId } from '@/remote/activitypub/type.js';
|
||||
|
||||
export default async function checkFetch(req: IncomingMessage): Promise<number> {
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
let signature;
|
||||
|
||||
try {
|
||||
signature = httpSignature.parseRequest(req, { 'headers': [] });
|
||||
} catch (e) {
|
||||
return 401;
|
||||
}
|
||||
|
||||
const keyId = new URL(signature.keyId);
|
||||
const host = toPuny(keyId.hostname);
|
||||
|
||||
if (meta.blockedHosts.includes(host)) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
if (meta.privateMode && host !== config.host && !meta.allowedHosts.includes(host)) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
const keyIdLower = signature.keyId.toLowerCase();
|
||||
if (keyIdLower.startsWith('acct:')) {
|
||||
// Old keyId is no longer supported.
|
||||
return 401;
|
||||
}
|
||||
|
||||
const dbResolver = new DbResolver();
|
||||
|
||||
// Get user from database based on HTTP-Signature keyId
|
||||
let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
|
||||
|
||||
// If keyid is unknown, try resolving it
|
||||
if (authUser == null) {
|
||||
try {
|
||||
keyId.hash = '';
|
||||
authUser = await dbResolver.getAuthUserFromApId(getApId(keyId.toString()));
|
||||
} catch (e) {
|
||||
return 403;
|
||||
}
|
||||
}
|
||||
|
||||
if (authUser?.key == null) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
if (authUser.user.host !== host) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
// HTTP-Signature validation
|
||||
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
||||
|
||||
if (!httpSignatureValidated) {
|
||||
return 403;
|
||||
}
|
||||
}
|
||||
return 200;
|
||||
}
|
|
@ -72,7 +72,11 @@ export default class Resolver {
|
|||
throw new Error('Instance is blocked');
|
||||
}
|
||||
|
||||
if (!this.user) {
|
||||
if (meta.privateMode && config.host !== host && !meta.allowedHosts.includes(host)) {
|
||||
throw new Error('Instance is not allowed');
|
||||
}
|
||||
|
||||
if (config.signToActivityPubGet && !this.user) {
|
||||
this.user = await getInstanceActor();
|
||||
}
|
||||
|
||||
|
|
|
@ -9,11 +9,14 @@ import renderKey from '@/remote/activitypub/renderer/key.js';
|
|||
import { renderPerson } from '@/remote/activitypub/renderer/person.js';
|
||||
import renderEmoji from '@/remote/activitypub/renderer/emoji.js';
|
||||
import { inbox as processInbox } from '@/queue/index.js';
|
||||
import { isSelfHost } from '@/misc/convert-host.js';
|
||||
import { isSelfHost, toPuny } from '@/misc/convert-host.js';
|
||||
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
|
||||
import { ILocalUser, User } from '@/models/entities/user.js';
|
||||
import { renderLike } from '@/remote/activitypub/renderer/like.js';
|
||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||
import { getInstanceActor } from '@/services/instance-actor.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||
import Outbox, { packActivity } from './activitypub/outbox.js';
|
||||
import Followers from './activitypub/followers.js';
|
||||
|
@ -66,6 +69,12 @@ router.post('/users/:user/inbox', json(), inbox);
|
|||
router.get('/notes/:note', async (ctx, next) => {
|
||||
if (!isActivityPubReq(ctx)) return await next();
|
||||
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await Notes.findOneBy({
|
||||
id: ctx.params.note,
|
||||
visibility: In(['public' as const, 'home' as const]),
|
||||
|
@ -78,7 +87,7 @@ router.get('/notes/:note', async (ctx, next) => {
|
|||
}
|
||||
|
||||
// リモートだったらリダイレクト
|
||||
if (note.userHost != null) {
|
||||
if (note.userHost !== null) {
|
||||
if (note.uri == null || isSelfHost(note.userHost)) {
|
||||
ctx.status = 500;
|
||||
return;
|
||||
|
@ -88,12 +97,24 @@ router.get('/notes/:note', async (ctx, next) => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderNote(note, false));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
// note activity
|
||||
router.get('/notes/:note/activity', async ctx => {
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await Notes.findOneBy({
|
||||
id: ctx.params.note,
|
||||
userHost: IsNull(),
|
||||
|
@ -107,7 +128,12 @@ router.get('/notes/:note/activity', async ctx => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await packActivity(note));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
|
@ -125,6 +151,20 @@ router.get('/users/:user/collections/featured', Featured);
|
|||
|
||||
// publickey
|
||||
router.get('/users/:user/publickey', async ctx => {
|
||||
const instanceActor = await getInstanceActor();
|
||||
if (ctx.params.user === instanceActor.id) {
|
||||
ctx.body = renderActivity(renderKey(instanceActor, await getUserKeypair(instanceActor.id)));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = ctx.params.user;
|
||||
|
||||
const user = await Users.findOneBy({
|
||||
|
@ -141,7 +181,12 @@ router.get('/users/:user/publickey', async ctx => {
|
|||
|
||||
if (Users.isLocalUser(user)) {
|
||||
ctx.body = renderActivity(renderKey(user, keypair));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
setResponseType(ctx);
|
||||
} else {
|
||||
ctx.status = 400;
|
||||
|
@ -156,13 +201,30 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderPerson(user as ILocalUser));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
setResponseType(ctx);
|
||||
}
|
||||
|
||||
router.get('/users/:user', async (ctx, next) => {
|
||||
if (!isActivityPubReq(ctx)) return await next();
|
||||
|
||||
const instanceActor = await getInstanceActor();
|
||||
if (ctx.params.user === instanceActor.id) {
|
||||
await userInfo(ctx, instanceActor);
|
||||
return;
|
||||
}
|
||||
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = ctx.params.user;
|
||||
|
||||
const user = await Users.findOneBy({
|
||||
|
@ -177,6 +239,18 @@ router.get('/users/:user', async (ctx, next) => {
|
|||
router.get('/@:user', async (ctx, next) => {
|
||||
if (!isActivityPubReq(ctx)) return await next();
|
||||
|
||||
if (ctx.params.user === 'instance.actor') {
|
||||
const instanceActor = await getInstanceActor();
|
||||
await userInfo(ctx, instanceActor);
|
||||
return;
|
||||
}
|
||||
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await Users.findOneBy({
|
||||
usernameLower: ctx.params.user.toLowerCase(),
|
||||
host: IsNull(),
|
||||
|
@ -185,6 +259,11 @@ router.get('/@:user', async (ctx, next) => {
|
|||
|
||||
await userInfo(ctx, user);
|
||||
});
|
||||
|
||||
router.get('/actor', async (ctx, next) => {
|
||||
const instanceActor = await getInstanceActor();
|
||||
await userInfo(ctx, instanceActor);
|
||||
});
|
||||
//#endregion
|
||||
|
||||
// emoji
|
||||
|
@ -200,12 +279,23 @@ router.get('/emojis/:emoji', async ctx => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderEmoji(emoji));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
// like
|
||||
router.get('/likes/:like', async ctx => {
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await Notes.findOneBy({
|
||||
id: reaction.noteId,
|
||||
visibility: In(['public' as const, 'home' as const]),
|
||||
|
@ -224,12 +314,22 @@ router.get('/likes/:like', async ctx => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(await renderLike(reaction, note));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
// follow
|
||||
router.get('/follows/:follower/:followee', async ctx => {
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
// This may be used before the follow is completed, so we do not
|
||||
// check if the following exists.
|
||||
|
||||
|
@ -250,7 +350,12 @@ router.get('/follows/:follower/:followee', async ctx => {
|
|||
}
|
||||
|
||||
ctx.body = renderActivity(renderFollow(follower, followee));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
|
|
|
@ -6,8 +6,17 @@ import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-colle
|
|||
import renderNote from '@/remote/activitypub/renderer/note.js';
|
||||
import { Users, Notes, UserNotePinings } from '@/models/index.js';
|
||||
import { setResponseType } from '../activitypub.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
|
||||
export default async (ctx: Router.RouterContext) => {
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = ctx.params.user;
|
||||
|
||||
const user = await Users.findOneBy({
|
||||
|
@ -36,6 +45,12 @@ export default async (ctx: Router.RouterContext) => {
|
|||
);
|
||||
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
setResponseType(ctx);
|
||||
};
|
||||
|
|
|
@ -9,12 +9,20 @@ import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
|
|||
import { Users, Followings, UserProfiles } from '@/models/index.js';
|
||||
import { Following } from '@/models/entities/following.js';
|
||||
import { setResponseType } from '../activitypub.js';
|
||||
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
|
||||
export default async (ctx: Router.RouterContext) => {
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = ctx.params.user;
|
||||
|
||||
const cursor = ctx.request.query.cursor;
|
||||
if (cursor != null && typeof cursor !== 'string') {
|
||||
if (cursor !== null && typeof cursor !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
@ -89,7 +97,12 @@ export default async (ctx: Router.RouterContext) => {
|
|||
// index page
|
||||
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
}
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,12 +9,20 @@ import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
|
|||
import { Users, Followings, UserProfiles } from '@/models/index.js';
|
||||
import { Following } from '@/models/entities/following.js';
|
||||
import { setResponseType } from '../activitypub.js';
|
||||
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
|
||||
export default async (ctx: Router.RouterContext) => {
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = ctx.params.user;
|
||||
|
||||
const cursor = ctx.request.query.cursor;
|
||||
if (cursor != null && typeof cursor !== 'string') {
|
||||
if (cursor !== null && typeof cursor !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
@ -89,7 +97,12 @@ export default async (ctx: Router.RouterContext) => {
|
|||
// index page
|
||||
const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
}
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
};
|
||||
|
|
|
@ -14,25 +14,33 @@ import { Note } from '@/models/entities/note.js';
|
|||
import { isPureRenote } from '@/misc/renote.js';
|
||||
import { makePaginationQuery } from '../api/common/make-pagination-query.js';
|
||||
import { setResponseType } from '../activitypub.js';
|
||||
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
|
||||
export default async (ctx: Router.RouterContext) => {
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = ctx.params.user;
|
||||
|
||||
const sinceId = ctx.request.query.since_id;
|
||||
if (sinceId != null && typeof sinceId !== 'string') {
|
||||
if (sinceId !== null && typeof sinceId !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
const untilId = ctx.request.query.until_id;
|
||||
if (untilId != null && typeof untilId !== 'string') {
|
||||
if (untilId !== null && typeof untilId !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
const page = ctx.request.query.page === 'true';
|
||||
|
||||
if (countIf(x => x != null, [sinceId, untilId]) > 1) {
|
||||
if (countIf(x => x !== null, [sinceId, untilId]) > 1) {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
@ -90,9 +98,15 @@ export default async (ctx: Router.RouterContext) => {
|
|||
`${partOf}?page=true&since_id=000000000000000000000000`,
|
||||
);
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
|
||||
setResponseType(ctx);
|
||||
}
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set('Cache-Control', 'no-store');
|
||||
} else {
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,8 @@ import { limiter } from './limiter.js';
|
|||
import endpoints, { IEndpointMeta } from './endpoints.js';
|
||||
import { ApiError } from './error.js';
|
||||
import { apiLogger } from './logger.js';
|
||||
import { AccessToken } from '@/models/entities/access-token.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
|
||||
const accessDenied = {
|
||||
message: 'Access denied.',
|
||||
|
@ -93,6 +95,17 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
|||
});
|
||||
}
|
||||
|
||||
// private mode
|
||||
const meta = await fetchMeta();
|
||||
if (meta.privateMode && ep.meta.requireCredentialPrivateMode && user == null) {
|
||||
throw new ApiError({
|
||||
message: 'Credential required.',
|
||||
code: 'CREDENTIAL_REQUIRED',
|
||||
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
|
||||
httpStatusCode: 401
|
||||
});
|
||||
}
|
||||
|
||||
// Cast non JSON input
|
||||
if ((ep.meta.requireFile || ctx?.method === 'GET') && ep.params.properties) {
|
||||
for (const k of Object.keys(ep.params.properties)) {
|
||||
|
|
|
@ -692,6 +692,12 @@ export interface IEndpointMeta {
|
|||
*/
|
||||
readonly secure?: boolean;
|
||||
|
||||
/**
|
||||
* If in private mode, whether credentials are required when making a request to this endpoint.
|
||||
* If omitted, this is interpreted as false.
|
||||
*/
|
||||
readonly requireCredentialPrivateMode?: boolean;
|
||||
|
||||
/**
|
||||
* エンドポイントの種類
|
||||
* パーミッションの実現に利用されます。
|
||||
|
|
|
@ -153,6 +153,22 @@ export const meta = {
|
|||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
allowedHosts: {
|
||||
type: 'array',
|
||||
optional: true, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
privateMode: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
secureMode: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
hcaptchaSecretKey: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
|
@ -327,6 +343,9 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
pinnedUsers: instance.pinnedUsers,
|
||||
hiddenTags: instance.hiddenTags,
|
||||
blockedHosts: instance.blockedHosts,
|
||||
allowedHosts: instance.allowedHosts,
|
||||
privateMode: instance.privateMode,
|
||||
secureMode: instance.secureMode,
|
||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||
proxyAccountId: instance.proxyAccountId,
|
||||
|
|
|
@ -26,6 +26,11 @@ export const paramDef = {
|
|||
blockedHosts: { type: 'array', nullable: true, items: {
|
||||
type: 'string',
|
||||
} },
|
||||
allowedHosts: { type: 'array', nullable: true, items: {
|
||||
type: 'string',
|
||||
} },
|
||||
secureMode: { type: 'boolean', nullable: true },
|
||||
privateMode: { type: 'boolean', nullable: true },
|
||||
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
|
||||
bannerUrl: { type: 'string', nullable: true },
|
||||
iconUrl: { type: 'string', nullable: true },
|
||||
|
@ -131,6 +136,18 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.themeColor = ps.themeColor;
|
||||
}
|
||||
|
||||
if (Array.isArray(ps.allowedHosts)) {
|
||||
set.allowedHosts = ps.allowedHosts.filter(Boolean);
|
||||
}
|
||||
|
||||
if (typeof ps.privateMode === 'boolean') {
|
||||
set.privateMode = ps.privateMode;
|
||||
}
|
||||
|
||||
if (typeof ps.secureMode === 'boolean') {
|
||||
set.secureMode = ps.secureMode;
|
||||
}
|
||||
|
||||
if (ps.bannerUrl !== undefined) {
|
||||
set.bannerUrl = ps.bannerUrl;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ export const meta = {
|
|||
tags: ['meta'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -5,6 +5,7 @@ export const meta = {
|
|||
tags: ['channels'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -6,6 +6,7 @@ export const meta = {
|
|||
tags: ['channels'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
|
|
@ -8,6 +8,7 @@ export const meta = {
|
|||
tags: ['notes', 'channels'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts', 'users'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(activeUsersChart.schema),
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(apRequestChart.schema),
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts', 'drive'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(driveChart.schema),
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(federationChart.schema),
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts', 'hashtags'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(hashtagChart.schema),
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(instanceChart.schema),
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts', 'notes'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(notesChart.schema),
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts', 'drive', 'users'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(perUserDriveChart.schema),
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts', 'users', 'following'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(perUserFollowingChart.schema),
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts', 'users', 'notes'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(perUserNotesChart.schema),
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts', 'users', 'reactions'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(perUserReactionsChart.schema),
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['charts', 'users'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: getJsonSchema(usersChart.schema),
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ export const meta = {
|
|||
tags: ['account', 'notes', 'clips'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ export const meta = {
|
|||
tags: ['clips', 'account'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ export const meta = {
|
|||
tags: ['federation'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -6,6 +6,7 @@ export const meta = {
|
|||
tags: ['federation'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -6,6 +6,7 @@ export const meta = {
|
|||
tags: ['federation'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -6,6 +6,7 @@ export const meta = {
|
|||
tags: ['federation'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
oneOf: [{
|
||||
|
|
|
@ -6,6 +6,7 @@ export const meta = {
|
|||
tags: ['federation'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -5,6 +5,7 @@ export const meta = {
|
|||
tags: ['gallery'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -5,6 +5,7 @@ export const meta = {
|
|||
tags: ['gallery'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -4,6 +4,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -6,6 +6,7 @@ export const meta = {
|
|||
tags: ['gallery'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
errors: {
|
||||
noSuchPost: {
|
||||
|
|
|
@ -7,6 +7,7 @@ export const meta = {
|
|||
tags: ['meta'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
|
|
@ -5,6 +5,7 @@ export const meta = {
|
|||
tags: ['hashtags'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -5,6 +5,7 @@ export const meta = {
|
|||
tags: ['hashtags'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -7,6 +7,7 @@ export const meta = {
|
|||
tags: ['hashtags'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
|
|
@ -24,6 +24,7 @@ export const meta = {
|
|||
tags: ['hashtags'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../../define.js';
|
|||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
tags: ['hashtags', 'users'],
|
||||
|
||||
|
|
|
@ -249,6 +249,16 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
},
|
||||
secureMode: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
default: false,
|
||||
},
|
||||
privateMode: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -267,7 +277,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
export default define(meta, paramDef, async (ps, me): Promise<Record<string, any>> => {
|
||||
const instance = await fetchMeta(true);
|
||||
|
||||
const emojis = await Emojis.find({
|
||||
|
@ -284,7 +294,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
},
|
||||
});
|
||||
|
||||
return {
|
||||
const response: Record<string, any> = {
|
||||
maintainerName: instance.maintainerName,
|
||||
maintainerEmail: instance.maintainerEmail,
|
||||
|
||||
|
@ -295,6 +305,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
description: instance.description,
|
||||
langs: instance.langs,
|
||||
tosUrl: instance.ToSUrl,
|
||||
|
||||
secureMode: instance.secureMode,
|
||||
privateMode: instance.privateMode,
|
||||
|
||||
disableRegistration: instance.disableRegistration,
|
||||
disableLocalTimeline: instance.disableLocalTimeline,
|
||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||
|
@ -312,7 +326,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
backgroundImageUrl: instance.backgroundImageUrl,
|
||||
logoImageUrl: instance.logoImageUrl,
|
||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
|
||||
emojis: await Emojis.packMany(emojis),
|
||||
emojis: instance.privateMode && !me ? [] : await Emojis.packMany(emojis),
|
||||
defaultLightTheme: instance.defaultLightTheme,
|
||||
defaultDarkTheme: instance.defaultDarkTheme,
|
||||
enableEmail: instance.enableEmail,
|
||||
|
@ -325,16 +339,20 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
translatorAvailable: instance.deeplAuthKey != null,
|
||||
|
||||
pinnedPages: instance.pinnedPages,
|
||||
pinnedClipId: instance.pinnedClipId,
|
||||
pinnedPages: instance.privateMode && !me ? [] : instance.pinnedPages,
|
||||
pinnedClipId: instance.privateMode && !me ? [] : instance.pinnedClipId,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
requireSetup: (await Users.countBy({
|
||||
host: IsNull(),
|
||||
})) === 0,
|
||||
};
|
||||
|
||||
proxyAccountName: instance.proxyAccountId ? (await Users.pack(instance.proxyAccountId).catch(() => null))?.username : null,
|
||||
|
||||
features: {
|
||||
if (ps.detail) {
|
||||
if (!instance.privateMode || me) {
|
||||
const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null;
|
||||
response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
|
||||
}
|
||||
response.features = {
|
||||
registration: !instance.disableRegistration,
|
||||
localTimeLine: !instance.disableLocalTimeline,
|
||||
globalTimeLine: !instance.disableGlobalTimeline,
|
||||
|
@ -348,6 +366,8 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
discord: instance.enableDiscordIntegration,
|
||||
serviceWorker: instance.enableServiceWorker,
|
||||
miauth: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import { makePaginationQuery } from '../common/make-pagination-query.js';
|
|||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredentialPrivateMode: true,
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
@ -9,6 +9,7 @@ export const meta = {
|
|||
tags: ['notes'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Get a list of children of a notes. Children includes replies as well as quote renotes that quote the respective post. A post will not be duplicated if it is a reply and a quote of a note in this thread. For depths larger than 1 the threading has to be computed by the client.',
|
||||
|
||||
|
@ -21,7 +22,7 @@ export const meta = {
|
|||
ref: 'Note',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
|
|
|
@ -8,6 +8,7 @@ export const meta = {
|
|||
tags: ['clips', 'notes'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -8,6 +8,7 @@ export const meta = {
|
|||
tags: ['notes'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -7,6 +7,7 @@ export const meta = {
|
|||
tags: ['notes'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -13,6 +13,7 @@ import { generateMutedRenotesQuery } from '../../common/generated-muted-renote-q
|
|||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredentialPrivateMode: true,
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
@ -15,6 +15,7 @@ import { generateMutedRenotesQuery } from '../../common/generated-muted-renote-q
|
|||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -9,6 +9,7 @@ export const meta = {
|
|||
tags: ['notes', 'reactions'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
allowGet: true,
|
||||
cacheSec: 60,
|
||||
|
|
|
@ -11,6 +11,7 @@ export const meta = {
|
|||
tags: ['notes'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -9,6 +9,7 @@ export const meta = {
|
|||
tags: ['notes'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -10,6 +10,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['notes', 'hashtags'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -12,6 +12,7 @@ export const meta = {
|
|||
tags: ['notes'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -7,6 +7,7 @@ export const meta = {
|
|||
tags: ['notes'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
|
|
@ -12,6 +12,7 @@ export const meta = {
|
|||
tags: ['notes'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
|
|
@ -5,6 +5,7 @@ export const meta = {
|
|||
tags: ['pages'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -8,6 +8,7 @@ export const meta = {
|
|||
tags: ['pages'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
|
|
|
@ -9,6 +9,7 @@ export const meta = {
|
|||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../define.js';
|
|||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
tags: ['meta'],
|
||||
} as const;
|
||||
|
|
|
@ -4,6 +4,7 @@ import define from '../define.js';
|
|||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
tags: ['meta'],
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export const meta = {
|
|||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
|
|
|
@ -4,6 +4,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['users', 'clips'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Show all clips this user owns.',
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ export const meta = {
|
|||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Show everyone that follows this user.',
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ export const meta = {
|
|||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Show everyone that this user is following.',
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { makePaginationQuery } from '../../../common/make-pagination-query.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['users', 'gallery'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Show all gallery posts by the given user.',
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ export const meta = {
|
|||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Get a list of other users that the specified user frequently replies to.',
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
|||
export const meta = {
|
||||
tags: ['users', 'notes'],
|
||||
|
||||
requireCredentialPrivateMode: true,
|
||||
description: 'Show all notes that this user created.',
|
||||
|
||||
res: {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
|||
|
||||
export const meta = {
|
||||
tags: ['users', 'pages'],
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Show all pages this user created.',
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ export const meta = {
|
|||
tags: ['users', 'reactions'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Show all reactions this user made.',
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export const meta = {
|
|||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Search for a user by username and/or host.',
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export const meta = {
|
|||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Search for users.',
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ export const meta = {
|
|||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Show the properties of a user.',
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export const meta = {
|
|||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
description: 'Show statistics about a user.',
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import { getNoteSummary } from '@/misc/get-note-summary.js';
|
|||
import { queues } from '@/queue/queues.js';
|
||||
import { MINUTE, DAY } from '@/const.js';
|
||||
import { genOpenapiSpec } from '../api/openapi/gen-spec.js';
|
||||
import meta from '../api/endpoints/meta.js';
|
||||
import { urlPreviewHandler } from './url-preview.js';
|
||||
import { manifestHandler } from './manifest.js';
|
||||
import packFeed from './feed.js';
|
||||
|
@ -218,6 +219,10 @@ router.get('/api.json', async ctx => {
|
|||
});
|
||||
|
||||
const getFeed = async (acct: string) => {
|
||||
const meta = await fetchMeta();
|
||||
if (meta.privateMode) {
|
||||
return;
|
||||
}
|
||||
const { username, host } = Acct.parse(acct);
|
||||
const user = await Users.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
|
@ -267,6 +272,12 @@ router.get('/@:user.json', async ctx => {
|
|||
//#region SSR (for crawlers)
|
||||
// User
|
||||
router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
|
||||
const meta = await fetchMeta();
|
||||
if (meta.privateMode) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
const { username, host } = Acct.parse(ctx.params.user);
|
||||
const user = await Users.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
|
@ -355,6 +366,12 @@ router.get('/notes/:note', async (ctx, next) => {
|
|||
|
||||
// Page
|
||||
router.get('/@:user/pages/:page', async (ctx, next) => {
|
||||
const meta = await fetchMeta();
|
||||
if (meta.privateMode) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
const { username, host } = Acct.parse(ctx.params.user);
|
||||
const user = await Users.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
|
@ -396,6 +413,12 @@ router.get('/@:user/pages/:page', async (ctx, next) => {
|
|||
// Clip
|
||||
// TODO: 非publicなclipのハンドリング
|
||||
router.get('/clips/:clip', async (ctx, next) => {
|
||||
const meta = await fetchMeta();
|
||||
if (meta.privateMode) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
const clip = await Clips.findOneBy({
|
||||
id: ctx.params.clip,
|
||||
});
|
||||
|
@ -409,6 +432,7 @@ router.get('/clips/:clip', async (ctx, next) => {
|
|||
profile,
|
||||
avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: clip.userId })),
|
||||
instanceName: meta.name || 'FoundKey',
|
||||
privateMode: meta.privateMode,
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
});
|
||||
|
@ -423,6 +447,12 @@ router.get('/clips/:clip', async (ctx, next) => {
|
|||
|
||||
// Gallery post
|
||||
router.get('/gallery/:post', async (ctx, next) => {
|
||||
const meta = await fetchMeta();
|
||||
if (meta.privateMode) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
const post = await GalleryPosts.findOneBy({ id: ctx.params.post });
|
||||
|
||||
if (post) {
|
||||
|
@ -448,6 +478,12 @@ router.get('/gallery/:post', async (ctx, next) => {
|
|||
|
||||
// Channel
|
||||
router.get('/channels/:channel', async (ctx, next) => {
|
||||
const meta = await fetchMeta();
|
||||
if (meta.privateMode) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = await Channels.findOneBy({
|
||||
id: ctx.params.channel,
|
||||
});
|
||||
|
@ -473,6 +509,10 @@ router.get('/channels/:channel', async (ctx, next) => {
|
|||
|
||||
router.get('/_info_card_', async ctx => {
|
||||
const meta = await fetchMeta(true);
|
||||
if (meta.privateMode) {
|
||||
ctx.status = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.remove('X-Frame-Options');
|
||||
|
||||
|
@ -513,6 +553,10 @@ router.get('/streaming', async ctx => {
|
|||
// Render base html for all requests
|
||||
router.get('(.*)', async ctx => {
|
||||
const meta = await fetchMeta();
|
||||
if (meta.privateMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.render('base', {
|
||||
img: meta.bannerUrl,
|
||||
title: meta.name || 'FoundKey',
|
||||
|
|
|
@ -26,6 +26,26 @@
|
|||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #label>{{ i18n.ts.instanceSecurity }}</template>
|
||||
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-if="!privateMode" v-model="secureMode">
|
||||
<template #label>{{ i18n.ts.secureMode }}</template>
|
||||
<template #caption>{{ i18n.ts.secureModeInfo }}</template>
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="privateMode">
|
||||
<template #label>{{ i18n.ts.privateMode }}</template>
|
||||
<template #caption>{{ i18n.ts.privateModeInfo }}</template>
|
||||
</FormSwitch>
|
||||
<FormTextarea v-if="privateMode" v-model="allowedHosts">
|
||||
<template #label>{{ i18n.ts.allowedInstances }}</template>
|
||||
<template #caption>{{ i18n.ts.allowedInstancesDescription }}</template>
|
||||
</FormTextarea>
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormFolder>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
|
@ -38,6 +58,7 @@ import FormFolder from '@/components/form/folder.vue';
|
|||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import * as os from '@/os';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
@ -47,16 +68,27 @@ let summalyProxy: string = $ref('');
|
|||
let enableHcaptcha: boolean = $ref(false);
|
||||
let enableRecaptcha: boolean = $ref(false);
|
||||
|
||||
let secureMode: boolean = $ref(false);
|
||||
let privateMode: boolean = $ref(false);
|
||||
let allowedHosts: string = $ref('');
|
||||
|
||||
async function init(): Promise<void> {
|
||||
const meta = await os.api('admin/meta');
|
||||
summalyProxy = meta.summalyProxy;
|
||||
enableHcaptcha = meta.enableHcaptcha;
|
||||
enableRecaptcha = meta.enableRecaptcha;
|
||||
|
||||
secureMode = meta.secureMode;
|
||||
privateMode = meta.privateMode;
|
||||
allowedHosts = meta.allowedHosts.join('\n');
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
summalyProxy,
|
||||
secureMode,
|
||||
privateMode,
|
||||
allowedHosts: allowedHosts.split('\n'),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue