From e1a5d195448e3d741b77fb982ce515489a360562 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Thu, 14 Nov 2024 17:39:06 +0100 Subject: [PATCH] fix: prevent deletion of last admin account --- backend/src/user/user.controller.ts | 13 +++++---- backend/src/user/user.service.ts | 40 +++++++++++++++++----------- frontend/src/pages/account/index.tsx | 26 +++++++++--------- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/backend/src/user/user.controller.ts b/backend/src/user/user.controller.ts index 38b2bf65..262ca401 100644 --- a/backend/src/user/user.controller.ts +++ b/backend/src/user/user.controller.ts @@ -3,6 +3,7 @@ import { Controller, Delete, Get, + HttpCode, Param, Patch, Post, @@ -14,18 +15,18 @@ import { Response } from "express"; import { GetUser } from "src/auth/decorator/getUser.decorator"; import { AdministratorGuard } from "src/auth/guard/isAdmin.guard"; import { JwtGuard } from "src/auth/guard/jwt.guard"; +import { ConfigService } from "../config/config.service"; import { CreateUserDTO } from "./dto/createUser.dto"; import { UpdateOwnUserDTO } from "./dto/updateOwnUser.dto"; import { UpdateUserDto } from "./dto/updateUser.dto"; import { UserDTO } from "./dto/user.dto"; import { UserSevice } from "./user.service"; -import { ConfigService } from "../config/config.service"; @Controller("users") export class UserController { constructor( private userService: UserSevice, - private config: ConfigService, + private config: ConfigService ) {} // Own user operations @@ -42,17 +43,20 @@ export class UserController { @UseGuards(JwtGuard) async updateCurrentUser( @GetUser() user: User, - @Body() data: UpdateOwnUserDTO, + @Body() data: UpdateOwnUserDTO ) { return new UserDTO().from(await this.userService.update(user.id, data)); } @Delete("me") + @HttpCode(204) @UseGuards(JwtGuard) async deleteCurrentUser( @GetUser() user: User, - @Res({ passthrough: true }) response: Response, + @Res({ passthrough: true }) response: Response ) { + await this.userService.delete(user.id); + const isSecure = this.config.get("general.secureCookies"); response.cookie("access_token", "accessToken", { @@ -65,7 +69,6 @@ export class UserController { maxAge: -1, secure: isSecure, }); - return new UserDTO().from(await this.userService.delete(user.id)); } // Global user operations diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts index b2003ef2..82ac5cc2 100644 --- a/backend/src/user/user.service.ts +++ b/backend/src/user/user.service.ts @@ -2,15 +2,15 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; import * as argon from "argon2"; import * as crypto from "crypto"; +import { Entry } from "ldapts"; +import { AuthSignInDTO } from "src/auth/dto/authSignIn.dto"; import { EmailService } from "src/email/email.service"; import { PrismaService } from "src/prisma/prisma.service"; +import { inspect } from "util"; +import { ConfigService } from "../config/config.service"; import { FileService } from "../file/file.service"; import { CreateUserDTO } from "./dto/createUser.dto"; import { UpdateUserDto } from "./dto/updateUser.dto"; -import { ConfigService } from "../config/config.service"; -import { Entry } from "ldapts"; -import { AuthSignInDTO } from "src/auth/dto/authSignIn.dto"; -import { inspect } from "util"; @Injectable() export class UserSevice { @@ -20,7 +20,7 @@ export class UserSevice { private prisma: PrismaService, private emailService: EmailService, private fileService: FileService, - private configService: ConfigService, + private configService: ConfigService ) {} async list() { @@ -55,7 +55,7 @@ export class UserSevice { if (e.code == "P2002") { const duplicatedField: string = e.meta.target[0]; throw new BadRequestException( - `A user with this ${duplicatedField} already exists`, + `A user with this ${duplicatedField} already exists` ); } } @@ -75,7 +75,7 @@ export class UserSevice { if (e.code == "P2002") { const duplicatedField: string = e.meta.target[0]; throw new BadRequestException( - `A user with this ${duplicatedField} already exists`, + `A user with this ${duplicatedField} already exists` ); } } @@ -89,8 +89,18 @@ export class UserSevice { }); if (!user) throw new BadRequestException("User not found"); + if (user.isAdmin) { + const userCount = await this.prisma.user.count({ + where: { isAdmin: true }, + }); + + if (userCount === 1) { + throw new BadRequestException("Cannot delete the last admin user"); + } + } + await Promise.all( - user.shares.map((share) => this.fileService.deleteAllFiles(share.id)), + user.shares.map((share) => this.fileService.deleteAllFiles(share.id)) ); return await this.prisma.user.delete({ where: { id } }); @@ -98,7 +108,7 @@ export class UserSevice { async findOrCreateFromLDAP( providedCredentials: AuthSignInDTO, - ldapEntry: Entry, + ldapEntry: Entry ) { const fieldNameMemberOf = this.configService.get("ldap.fieldNameMemberOf"); const fieldNameEmail = this.configService.get("ldap.fieldNameEmail"); @@ -112,7 +122,7 @@ export class UserSevice { isAdmin = entryGroups.includes(adminGroup) ?? false; } else { this.logger.warn( - `Trying to create/update a ldap user but the member field ${fieldNameMemberOf} is not present.`, + `Trying to create/update a ldap user but the member field ${fieldNameMemberOf} is not present.` ); } @@ -126,7 +136,7 @@ export class UserSevice { } } else { this.logger.warn( - `Trying to create/update a ldap user but the email field ${fieldNameEmail} is not present.`, + `Trying to create/update a ldap user but the email field ${fieldNameEmail} is not present.` ); } @@ -174,7 +184,7 @@ export class UserSevice { }) .catch((error) => { this.logger.warn( - `Failed to update users ${user.id} placeholder username: ${inspect(error)}`, + `Failed to update users ${user.id} placeholder username: ${inspect(error)}` ); }); } @@ -192,13 +202,13 @@ export class UserSevice { }) .then((newUser) => { this.logger.log( - `Updated users ${user.id} email from ldap from ${user.email} to ${userEmail}.`, + `Updated users ${user.id} email from ldap from ${user.email} to ${userEmail}.` ); user.email = newUser.email; }) .catch((error) => { this.logger.error( - `Failed to update users ${user.id} email to ${userEmail}: ${inspect(error)}`, + `Failed to update users ${user.id} email to ${userEmail}: ${inspect(error)}` ); }); } @@ -209,7 +219,7 @@ export class UserSevice { if (e.code == "P2002") { const duplicatedField: string = e.meta.target[0]; throw new BadRequestException( - `A user with this ${duplicatedField} already exists`, + `A user with this ${duplicatedField} already exists` ); } } diff --git a/frontend/src/pages/account/index.tsx b/frontend/src/pages/account/index.tsx index dc795e6e..4bb46dd7 100644 --- a/frontend/src/pages/account/index.tsx +++ b/frontend/src/pages/account/index.tsx @@ -56,7 +56,7 @@ const Account = () => { username: yup .string() .min(3, t("common.error.too-short", { length: 3 })), - }), + }) ), }); @@ -79,7 +79,7 @@ const Account = () => { .string() .min(8, t("common.error.too-short", { length: 8 })) .required(t("common.error.field-required")), - }), + }) ), }); @@ -93,7 +93,7 @@ const Account = () => { .string() .min(8, t("common.error.too-short", { length: 8 })) .required(t("common.error.field-required")), - }), + }) ), }); @@ -110,7 +110,7 @@ const Account = () => { .min(6, t("common.error.exact-length", { length: 6 })) .max(6, t("common.error.exact-length", { length: 6 })) .matches(/^[0-9]+$/, { message: t("common.error.invalid-number") }), - }), + }) ), }); @@ -155,7 +155,7 @@ const Account = () => { email: values.email, }) .then(() => toast.success(t("account.notify.info.success"))) - .catch(toast.axiosError), + .catch(toast.axiosError) )} > @@ -193,7 +193,7 @@ const Account = () => { toast.success(t("account.notify.password.success")); passwordForm.reset(); }) - .catch(toast.axiosError), + .catch(toast.axiosError) )} > @@ -265,7 +265,7 @@ const Account = () => { unlinkOAuth(provider) .then(() => { toast.success( - t("account.notify.oauth.unlinked.success"), + t("account.notify.oauth.unlinked.success") ); refreshOAuthStatus(); }) @@ -281,7 +281,7 @@ const Account = () => { component="a" href={getOAuthUrl( config.get("general.appUrl"), - provider, + provider )} > {t("account.card.oauth.link")} @@ -324,7 +324,7 @@ const Account = () => { { @@ -414,8 +414,10 @@ const Account = () => { }, confirmProps: { color: "red" }, onConfirm: async () => { - await userService.removeCurrentUser(); - window.location.reload(); + await userService + .removeCurrentUser() + .then(window.location.reload) + .catch(toast.axiosError); }, }) }