🚧 backend: wip initial setup for admin route to delete user (#799)

This commit is contained in:
Eric Doughty-Papassideris
2025-02-24 01:06:02 +01:00
committed by Anton Shepilov
parent 185ec5ac5c
commit 16c68f25fd
19 changed files with 500 additions and 4 deletions
@@ -0,0 +1,13 @@
import config from "../../../config";
interface IAdminConfig {
// This secret must be provided to the administration endpoints
endpointSecret?: string;
}
export const getConfig = (): IAdminConfig => {
const configSection = config.get("admin") as IAdminConfig;
return {
...configSection,
};
};
@@ -0,0 +1,92 @@
import gr from "../../../../../services/global-resolver";
import { getLogger } from "../../../../../core/platform/framework";
import User, { TYPE as UserType } from "../../../../../services/user/entities/user";
import type { DatabaseServiceAPI } from "../../database/api";
import type { ExecutionContext } from "../../../../platform/framework/api/crud-service";
import type { SearchServiceAPI } from "../../search/api";
import {
DriveFile,
TYPE as DriveFileType,
} from "../../../../../services/documents/entities/drive-file";
import { File } from "../../../../../services/files/entities/file";
import {
FileVersion,
TYPE as FileVersionType,
} from "../../../../../services/documents/entities/file-version";
import ExternalUser, {
TYPE as ExternalUserType,
} from "../../../../../services/user/entities/external_user";
import CompanyUser, {
TYPE as CompanyUserType,
} from "../../../../../services/user/entities/company_user";
const FileType = "files";
const logger = getLogger("AdminDeleteUserController");
/**
* Create all repositories required for deleting a user
* @deprecated Do not use this outside of this file, it is exported exclusively for e2e tests
*/
export async function buildUserDeletionRepositories(
db: DatabaseServiceAPI,
search: SearchServiceAPI,
) {
return {
driveFile: await db.getRepository<DriveFile>(DriveFileType, DriveFile),
file: await db.getRepository<File>(FileType, File),
fileVersion: await db.getRepository<FileVersion>(FileVersionType, FileVersion),
user: await db.getRepository<User>(UserType, User),
companyUser: await db.getRepository<CompanyUser>(CompanyUserType, CompanyUser),
externalUser: await db.getRepository<ExternalUser>(ExternalUserType, ExternalUser),
// group_entity
// group_user
// missed_drive_files
// session
// user
// user_online
search: {
driveFile: await search.getRepository<DriveFile>(DriveFileType, DriveFile),
user: await search.getRepository<User>(UserType, User),
},
};
}
export class AdminDeleteUserController {
private constructor(
private readonly repos: Awaited<ReturnType<typeof buildUserDeletionRepositories>>,
) {}
public static async create() {
return new AdminDeleteUserController(
await buildUserDeletionRepositories(gr.database, gr.platformServices.search),
);
}
// fisherYattesShuffleInPlace
/** Begin or forward the deletion process of a user */
async deleteUser(userId: string): Promise<"failed" | "deleting" | "done"> {
try {
await gr.services.users.anonymizeAndDelete({ id: userId }, {
user: { server_request: true },
company: { id: "// TODO: REPLACE WITH COMPANY ID" },
} as unknown as ExecutionContext);
const existingUser = await this.repos.user.findOne({ id: userId });
if (existingUser?.deleted) {
if (existingUser.delete_process_started_epoch > 0) return "deleting";
}
} catch (err) {
logger.error({ err, userId }, "User deletion error");
return "failed";
}
return "done";
}
/** Get an array of user IDs that are incompletely deleted */
async listUsersPendingDeletion() {
return (await this.repos.user.find({}, { $gt: [["delete_process_started_epoch", 0]] }))
.getEntities()
.map(({ id }) => id);
}
}
@@ -0,0 +1,36 @@
import { TdriveService, Consumes, Prefix, ServiceName } from "../../framework";
import web from "./web";
import AdminServiceAPI from "./service-provider";
import AdminServiceImpl from "./service";
import WebServerAPI from "../webserver/provider";
/**
* The admin service exposes endpoint that are of use for operational reasons to administrators only, and should not be exposed.
*/
@Prefix("/admin")
@Consumes(["webserver"])
@ServiceName("admin")
export default class AdminService extends TdriveService<AdminServiceAPI> {
name = "admin";
service: AdminServiceAPI;
api(): AdminServiceAPI {
return this.service;
}
public async doInit(): Promise<this> {
this.service = new AdminServiceImpl();
const fastify = this.context.getProvider<WebServerAPI>("webserver").getServer();
fastify.register((instance, _opts, next) => {
web(instance, { prefix: this.prefix });
next();
});
return this;
}
public async doStop(): Promise<this> {
return this;
}
}
@@ -0,0 +1,5 @@
import type { TdriveServiceProvider } from "../../framework";
type AdminServiceAPI = TdriveServiceProvider;
export default AdminServiceAPI;
@@ -0,0 +1,5 @@
import AdminServiceAPI from "./service-provider";
export default class AdminServiceImpl implements AdminServiceAPI {
version: "1";
}
@@ -0,0 +1,46 @@
import type { FastifyInstance, FastifyPluginCallback, FastifyReply, FastifyRequest } from "fastify";
import { getConfig } from "../../../framework/api/admin";
import { AdminDeleteUserController } from "../controller/delete-user-controller";
const config = getConfig();
type TQueryBody = { secret: string };
function authenticateAdminQuery(request: FastifyRequest, reply: FastifyReply) {
const body = request.body as TQueryBody;
if (body?.secret !== config.endpointSecret) {
reply.status(403).send();
return false;
}
return true;
}
type TUserDeleteQueryBody = TQueryBody & { userId: string };
function getUserIfValidQuery(request: FastifyRequest, reply: FastifyReply) {
if (!authenticateAdminQuery(request, reply)) return false;
const body = request.body as TUserDeleteQueryBody;
if (!body.userId?.length) {
reply.status(400).send();
return false;
}
return body.userId;
}
const routes: FastifyPluginCallback = async (fastify: FastifyInstance, _opts, next) => {
const config = getConfig();
const controller = await AdminDeleteUserController.create();
if (config?.endpointSecret?.length) {
fastify.post("/user/delete", async (request, reply) => {
const userId = getUserIfValidQuery(request, reply);
if (!userId) return false;
return reply.send({ status: await controller.deleteUser(userId) });
});
fastify.post("/user/delete/pending", async (request, reply) => {
if (!authenticateAdminQuery(request, reply)) return false;
return reply.send(await controller.listUsersPendingDeletion());
});
}
next();
};
export default routes;
@@ -0,0 +1,9 @@
import type { FastifyInstance, FastifyRegisterOptions } from "fastify";
import deleteUserRoutes from "./delete-user-routes";
export default (
fastify: FastifyInstance,
opts: FastifyRegisterOptions<{ prefix: string }>,
): void => {
fastify.register(deleteUserRoutes, opts);
};