server: properly handle logical deletion

closes FoundKeyGang/FoundKey#329
This commit is contained in:
Johann150 2023-03-22 18:29:19 +01:00
parent 71dfd229b0
commit b14f3e8cdc
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
6 changed files with 56 additions and 29 deletions

View file

@ -1,3 +1,4 @@
import { IsNull, Not } from 'typeorm';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { User } from '@/models/entities/user.js';
import { Note } from '@/models/entities/note.js';
@ -28,8 +29,12 @@ export async function getNote(noteId: Note['id'], me: { id: User['id'] } | null)
/**
* Get user for API processing
*/
export async function getUser(userId: User['id']) {
const user = await Users.findOneBy({ id: userId });
export async function getUser(userId: User['id'], includeSuspended = false) {
const user = await Users.findOneBy(
id: userId,
isDeleted: false,
isSuspended: !includeSuspended,
});
if (user == null) {
throw new ApiError('NO_SUCH_USER');
@ -41,10 +46,15 @@ export async function getUser(userId: User['id']) {
/**
* Get remote user for API processing
*/
export async function getRemoteUser(userId: User['id']) {
const user = await getUser(userId);
export async function getRemoteUser(userId: User['id'], includeSuspended = false) {
const user = await Users.findOneBy(
id: userId,
host: Not(IsNull()),
isDeleted: false,
isSuspended: !includedSuspended,
});
if (!Users.isRemoteUser(user)) {
if (user == null) {
throw new ApiError('NO_SUCH_USER');
}
@ -54,10 +64,15 @@ export async function getRemoteUser(userId: User['id']) {
/**
* Get local user for API processing
*/
export async function getLocalUser(userId: User['id']) {
const user = await getUser(userId);
export async function getLocalUser(userId: User['id'], includeSuspended = false) {
const user = await Users.findOneBy(
id: userId,
host: IsNull(),
isDeleted: false,
isSuspended: !includeSuspended,
});
if (!Users.isLocalUser(user)) {
if (user == null) {
throw new ApiError('NO_SUCH_USER');
}

View file

@ -2,6 +2,7 @@ import { Users } from '@/models/index.js';
import { ApiError } from '@/server/api/error.js';
import { deleteAccount } from '@/services/delete-account.js';
import define from '@/server/api/define.js';
import { getUser } from '@/server/api/common/getters.js';
export const meta = {
tags: ['admin'],
@ -22,14 +23,9 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps) => {
const user = await Users.findOneBy({
id: ps.userId,
isDeleted: false,
});
const user = await getUser(ps.userId, true);
if (user == null) {
throw new ApiError('NO_SUCH_USER');
} else if (user.isAdmin) {
if (user.isAdmin) {
throw new ApiError('IS_ADMIN');
} else if (user.isModerator) {
throw new ApiError('IS_MODERATOR');

View file

@ -47,6 +47,7 @@ export default async (ctx: Koa.Context) => {
const user = await Users.findOneBy({
usernameLower: username.toLowerCase(),
host: IsNull(),
isDeleted: false,
}) as ILocalUser;
if (user == null) {

View file

@ -223,6 +223,7 @@ const getFeed = async (acct: string) => {
usernameLower: username.toLowerCase(),
host: host ?? IsNull(),
isSuspended: false,
isDeleted: false,
});
return user && await packFeed(user);
@ -272,6 +273,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
usernameLower: username.toLowerCase(),
host: host ?? IsNull(),
isSuspended: false,
isDeleted: false,
});
if (user != null) {
@ -304,6 +306,7 @@ router.get('/users/:user', async ctx => {
id: ctx.params.user,
host: IsNull(),
isSuspended: false,
isDeleted: false,
});
if (user == null) {
@ -419,6 +422,8 @@ router.get('/@:user/pages/:page', async (ctx, next) => {
const user = await Users.findOneBy({
usernameLower: username.toLowerCase(),
host: host ?? IsNull(),
isSuspended: false,
isDeleted: false,
});
if (user == null) return;

View file

@ -1,4 +1,4 @@
import { Users } from '@/models/index.js';
import { AccessTokens, Users } from '@/models/index.js';
import { createDeleteAccountJob } from '@/queue/index.js';
import { publishUserEvent } from './stream.js';
import { doPostSuspend } from './suspend-user.js';
@ -7,9 +7,15 @@ export async function deleteAccount(user: {
id: string;
host: string | null;
}): Promise<void> {
await Users.update(user.id, {
await Promise.all([
Users.update(user.id, {
isDeleted: true,
});
}),
// revoke all of the users access tokens to block API access
AccessTokens.delete({
userId: user.id,
}),
]);
if (Users.isLocalUser(user)) {
// Terminate streaming

View file

@ -6,15 +6,15 @@ import { subscriber } from '@/db/redis.js';
export const userByIdCache = new Cache<User>(
Infinity,
async (id) => await Users.findOneBy({ id }) ?? undefined,
async (id) => await Users.findOneBy({ id, isDeleted: false }) ?? undefined,
);
export const localUserByNativeTokenCache = new Cache<CacheableLocalUser>(
Infinity,
async (token) => await Users.findOneBy({ token, host: IsNull() }) as ILocalUser | null ?? undefined,
async (token) => await Users.findOneBy({ token, host: IsNull(), isDeleted: false }) as ILocalUser | null ?? undefined,
);
export const uriPersonCache = new Cache<User>(
Infinity,
async (uri) => await Users.findOneBy({ uri }) ?? undefined,
async (uri) => await Users.findOneBy({ uri, isDeleted: false }) ?? undefined,
);
subscriber.on('message', async (_, data) => {
@ -28,15 +28,19 @@ subscriber.on('message', async (_, data) => {
case 'userChangeModeratorState':
case 'remoteUserUpdated': {
const user = await Users.findOneByOrFail({ id: body.id });
if (user.isDeleted) {
userByIdCache.delete(user.id);
uriPersonCache.delete(user.uri);
if (Users.isLocalUser(user)) {
localUserByNativeTokenCache.delete(user.token);
}
} else {
userByIdCache.set(user.id, user);
for (const [k, v] of uriPersonCache.cache.entries()) {
if (v.value.id === user.id) {
uriPersonCache.set(k, user);
}
}
uriPersonCache.set(user.uri, user);
if (Users.isLocalUser(user)) {
localUserByNativeTokenCache.set(user.token, user);
}
}
break;
}
case 'userTokenRegenerated': {