wip
This commit is contained in:
parent
dc1102510b
commit
21d6aacea8
@ -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"`);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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']) {
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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>);
|
||||||
|
@ -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,16 +49,12 @@ 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);
|
throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
|
||||||
|
}
|
||||||
|
|
||||||
if (ep.meta.requireAdmin && !fullUser.isAdmin) {
|
if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) {
|
||||||
throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
|
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
|
||||||
}
|
|
||||||
|
|
||||||
if (ep.meta.requireModerator && !fullUser.isAdmin && !fullUser.isModerator) {
|
|
||||||
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)) {
|
||||||
@ -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({
|
||||||
|
39
packages/backend/src/services/user-cache.ts
Normal file
39
packages/backend/src/services/user-cache.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user