🚧 backend: wip initial setup for admin route to delete user (#799)
This commit is contained in:
committed by
Anton Shepilov
parent
185ec5ac5c
commit
16c68f25fd
@@ -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,
|
||||
};
|
||||
};
|
||||
+92
@@ -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);
|
||||
};
|
||||
@@ -78,6 +78,12 @@ export default class User {
|
||||
@Column("deleted", "tdrive_boolean")
|
||||
deleted: boolean;
|
||||
|
||||
/**
|
||||
* If set, a restartable suppression process is currently incomplete.
|
||||
*/
|
||||
@Column("delete_process_started_epoch", "number")
|
||||
delete_process_started_epoch?: number;
|
||||
|
||||
@Column("mail_verified", "tdrive_boolean")
|
||||
mail_verified: boolean;
|
||||
|
||||
|
||||
@@ -310,7 +310,11 @@ export class CompanyServiceImpl {
|
||||
for (const company of companies) {
|
||||
logger.warn(`User ${userPk.id} is deleted so removed from company ${company.id}`);
|
||||
await this.removeUserFromCompany(company, user);
|
||||
await gr.services.workspaces.ensureUserNotInCompanyIsNotInWorkspace(userPk, company.id);
|
||||
try {
|
||||
await gr.services.workspaces.ensureUserNotInCompanyIsNotInWorkspace(userPk, company.id);
|
||||
} catch (err) {
|
||||
logger.error({ err }, "Error removing user from company from workspace");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ export class UserServiceImpl {
|
||||
user.thumbnail_id = null;
|
||||
user.status_icon = null;
|
||||
user.deleted = true;
|
||||
user.delete_process_started_epoch = new Date().getTime();
|
||||
|
||||
await this.save(user);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export const userObjectSchema = {
|
||||
last_name: { type: "string" },
|
||||
created_at: { type: "number" },
|
||||
deleted: { type: "boolean" },
|
||||
delete_process_started_epoch: { type: "number" },
|
||||
|
||||
status: { type: "string" },
|
||||
last_activity: { type: "number" },
|
||||
|
||||
@@ -732,9 +732,11 @@ export class WorkspaceServiceImpl implements TdriveServiceProvider, Initializabl
|
||||
logger.warn(
|
||||
`User ${userPk.id} is not in company ${workspace.company_id} so removing from workspace ${workspace.id}`,
|
||||
);
|
||||
this.removeUser({ workspaceId: workspace.id, userId: userPk.id }, companyId, context).then(
|
||||
() => null,
|
||||
);
|
||||
await this.removeUser(
|
||||
{ workspaceId: workspace.id, userId: userPk.id },
|
||||
companyId,
|
||||
context,
|
||||
).then(() => null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export async function formatUser(
|
||||
full_name: [user.first_name, user.last_name].join(" "),
|
||||
created_at: user.creation_date,
|
||||
deleted: Boolean(user.deleted),
|
||||
delete_process_started_epoch: user.delete_process_started_epoch,
|
||||
status: user.status_icon,
|
||||
last_activity: user.last_activity,
|
||||
cache: { companies: user.cache?.companies || [] },
|
||||
|
||||
Reference in New Issue
Block a user