This commit is contained in:
syuilo 2022-03-25 13:55:17 +09:00
parent dc1102510b
commit 21d6aacea8
8 changed files with 57 additions and 68 deletions

View File

@ -1,17 +0,0 @@
export class userIndexes1647777116829 {
name = 'userIndexes1647777116829'
async up(queryRunner) {
await queryRunner.query(`CREATE INDEX "IDX_8977c6037a7bc2cb0c84b6d4db" ON "user" ("isSuspended") `);
await queryRunner.query(`CREATE INDEX "IDX_391e03c755cc3dffa85d85536f" ON "user" ("isSilenced") `);
await queryRunner.query(`CREATE INDEX "IDX_b2033a3235871353c93700a0b6" ON "user" ("isAdmin") `);
await queryRunner.query(`CREATE INDEX "IDX_dfb7b092897e7b354f73e7ae25" ON "user" ("isModerator") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_dfb7b092897e7b354f73e7ae25"`);
await queryRunner.query(`DROP INDEX "public"."IDX_b2033a3235871353c93700a0b6"`);
await queryRunner.query(`DROP INDEX "public"."IDX_391e03c755cc3dffa85d85536f"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8977c6037a7bc2cb0c84b6d4db"`);
}
}

View File

@ -1,5 +1,5 @@
export class Cache<T> { export class Cache<T> {
private cache: Map<string | null, { date: number; value: T; }>; public cache: Map<string | null, { date: number; value: T; }>;
private lifetime: number; private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) { constructor(lifetime: Cache<never>['lifetime']) {

View File

@ -106,14 +106,12 @@ export class User {
}) })
public tags: string[]; public tags: string[];
@Index()
@Column('boolean', { @Column('boolean', {
default: false, default: false,
comment: 'Whether the User is suspended.', comment: 'Whether the User is suspended.',
}) })
public isSuspended: boolean; public isSuspended: boolean;
@Index()
@Column('boolean', { @Column('boolean', {
default: false, default: false,
comment: 'Whether the User is silenced.', comment: 'Whether the User is silenced.',
@ -138,14 +136,12 @@ export class User {
}) })
public isCat: boolean; public isCat: boolean;
@Index()
@Column('boolean', { @Column('boolean', {
default: false, default: false,
comment: 'Whether the User is the admin.', comment: 'Whether the User is the admin.',
}) })
public isAdmin: boolean; public isAdmin: boolean;
@Index()
@Column('boolean', { @Column('boolean', {
default: false, default: false,
comment: 'Whether the User is a moderator.', comment: 'Whether the User is a moderator.',
@ -239,28 +235,8 @@ export interface IRemoteUser extends User {
host: string; host: string;
} }
export type CacheableLocalUser = { export type CacheableLocalUser = ILocalUser;
id: ILocalUser['id'];
createdAt: ILocalUser['createdAt'];
host: ILocalUser['host'];
username: ILocalUser['username'];
uri: ILocalUser['uri'];
inbox: ILocalUser['inbox'];
sharedInbox: ILocalUser['sharedInbox'];
showTimelineReplies: ILocalUser['showTimelineReplies']; export type CacheableRemoteUser = IRemoteUser;
};
export type CacheableRemoteUser = {
id: IRemoteUser['id'];
createdAt: IRemoteUser['createdAt'];
host: IRemoteUser['host'];
username: IRemoteUser['username'];
uri: IRemoteUser['uri'];
inbox: IRemoteUser['inbox'];
sharedInbox: IRemoteUser['sharedInbox'];
featured: IRemoteUser['featured'];
followersUri: IRemoteUser['followersUri'];
};
export type CacheableUser = CacheableLocalUser | CacheableRemoteUser; export type CacheableUser = CacheableLocalUser | CacheableRemoteUser;

View File

@ -8,8 +8,9 @@ import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.
import { IObject, getApId } from './type.js'; import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.js'; import { resolvePerson } from './models/person.js';
import { Cache } from '@/misc/cache.js'; import { Cache } from '@/misc/cache.js';
import { userByIdCache } from '@/services/user-cache.js';
const publicKeyCache = new Cache<(UserPublickey & { user: User }) | null>(Infinity); const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity); const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
export default class DbResolver { export default class DbResolver {
@ -86,19 +87,17 @@ export default class DbResolver {
const key = await publicKeyCache.fetch(keyId, async () => { const key = await publicKeyCache.fetch(keyId, async () => {
const key = await UserPublickeys.findOne({ const key = await UserPublickeys.findOne({
keyId, keyId,
}, {
relations: ['user'],
}); });
if (key == null) return null; if (key == null) return null;
return key as UserPublickey & { user: User }; return key;
}, key => key != null); }, key => key != null);
if (key == null) return null; if (key == null) return null;
return { return {
user: key.user as IRemoteUser, user: await userByIdCache.fetch(key.userId, () => Users.findOneOrFail(key.userId)) as CacheableRemoteUser,
key, key,
}; };
} }

View File

@ -30,9 +30,7 @@ import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { truncate } from '@/misc/truncate.js'; import { truncate } from '@/misc/truncate.js';
import { StatusError } from '@/misc/fetch.js'; import { StatusError } from '@/misc/fetch.js';
import { Cache } from '@/misc/cache.js'; import { uriPersonCache } from '@/services/user-cache.js';
const uriPersonCache = new Cache<CacheableUser | null>(Infinity);
const logger = apLogger; const logger = apLogger;

View File

@ -1,12 +1,11 @@
import isNativeToken from './common/is-native-token.js'; import isNativeToken from './common/is-native-token.js';
import { CacheableLocalUser, ILocalUser, User } from '@/models/entities/user.js'; import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js';
import { Users, AccessTokens, Apps } from '@/models/index.js'; import { Users, AccessTokens, Apps } from '@/models/index.js';
import { AccessToken } from '@/models/entities/access-token.js'; import { AccessToken } from '@/models/entities/access-token.js';
import { Cache } from '@/misc/cache.js'; import { Cache } from '@/misc/cache.js';
import { App } from '@/models/entities/app.js'; import { App } from '@/models/entities/app.js';
import { localUserByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js';
const userByNativeTokenCache = new Cache<CacheableLocalUser | null>(1000 * 60 * 5);
const userByIdCache = new Cache<CacheableLocalUser>(1000 * 60 * 5);
const appCache = new Cache<App>(Infinity); const appCache = new Cache<App>(Infinity);
export class AuthenticationError extends Error { export class AuthenticationError extends Error {
@ -23,7 +22,7 @@ export default async (token: string | null): Promise<[CacheableLocalUser | null
if (isNativeToken(token)) { if (isNativeToken(token)) {
// TODO: typeorm 3.0にしたら .then(x => x || null) は消せる // TODO: typeorm 3.0にしたら .then(x => x || null) は消せる
const user = await userByNativeTokenCache.fetch(token, const user = await localUserByNativeTokenCache.fetch(token,
() => Users.findOne({ token }).then(x => x || null) as Promise<ILocalUser | null>); () => Users.findOne({ token }).then(x => x || null) as Promise<ILocalUser | null>);
if (user == null) { if (user == null) {
@ -48,7 +47,7 @@ export default async (token: string | null): Promise<[CacheableLocalUser | null
lastUsedAt: new Date(), lastUsedAt: new Date(),
}); });
const user = await userByIdCache.fetch(accessToken.userId, const user = await localUserByIdCache.fetch(accessToken.userId,
() => Users.findOne({ () => Users.findOne({
id: accessToken.userId, // findOne(accessToken.userId) のように書かないのは後方互換性のため id: accessToken.userId, // findOne(accessToken.userId) のように書かないのは後方互換性のため
}) as Promise<ILocalUser>); }) as Promise<ILocalUser>);

View File

@ -6,7 +6,6 @@ import endpoints, { IEndpoint } from './endpoints.js';
import { ApiError } from './error.js'; import { ApiError } from './error.js';
import { apiLogger } from './logger.js'; import { apiLogger } from './logger.js';
import { AccessToken } from '@/models/entities/access-token.js'; import { AccessToken } from '@/models/entities/access-token.js';
import { Users } from '@/models/index.js';
const accessDenied = { const accessDenied = {
message: 'Access denied.', message: 'Access denied.',
@ -50,17 +49,13 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
}); });
} }
if (ep.meta.requireAdmin || ep.meta.requireModerator) { if (ep.meta.requireAdmin && !user!.isAdmin) {
const fullUser = await Users.findOneOrFail(user!.id);
if (ep.meta.requireAdmin && !fullUser.isAdmin) {
throw new ApiError(accessDenied, { reason: 'You are not the admin.' }); throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
} }
if (ep.meta.requireModerator && !fullUser.isAdmin && !fullUser.isModerator) { if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) {
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
} }
}
if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
throw new ApiError({ throw new ApiError({
@ -70,7 +65,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
}); });
} }
if (ep.meta.requireCredential && ep.meta.limit) { if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) {
// Rate limit // Rate limit
await limiter(ep as IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user!).catch(e => { await limiter(ep as IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user!).catch(e => {
throw new ApiError({ throw new ApiError({

View File

@ -0,0 +1,39 @@
import { CacheableLocalUser, CacheableUser, User } from '@/models/entities/user.js';
import { Users } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import { subsdcriber } from '@/db/redis.js';
export const userByIdCache = new Cache<CacheableUser>(Infinity);
export const localUserByNativeTokenCache = new Cache<CacheableLocalUser | null>(Infinity);
export const localUserByIdCache = new Cache<CacheableLocalUser>(Infinity);
export const uriPersonCache = new Cache<CacheableUser | null>(Infinity);
subsdcriber.on('message', async (_, data) => {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message;
switch (type) {
case 'userChangeSuspendedState':
case 'userChangeSilencedState':
case 'userChangeModeratorState':
const user = await Users.findOneOrFail(body.id);
userByIdCache.set(user.id, user);
for (const [k, v] of uriPersonCache.cache.entries()) {
if (v.value?.id === user.id) {
uriPersonCache.set(k, user);
}
}
if (Users.isLocalUser(user)) {
localUserByNativeTokenCache.set(user.token, user);
localUserByIdCache.set(user.id, user);
}
break;
case 'userTokenRegenerated':
break;
default:
break;
}
}
});