1
0
mirror of https://github.com/stonith404/pingvin-share.git synced 2024-11-15 03:50:11 +01:00

fix: prevent deletion of last admin account

This commit is contained in:
Elias Schneider 2024-11-14 17:39:06 +01:00
parent 4ce64206be
commit e1a5d19544
No known key found for this signature in database
GPG Key ID: 07E623B294202B6C
3 changed files with 47 additions and 32 deletions

View File

@ -3,6 +3,7 @@ import {
Controller, Controller,
Delete, Delete,
Get, Get,
HttpCode,
Param, Param,
Patch, Patch,
Post, Post,
@ -14,18 +15,18 @@ import { Response } from "express";
import { GetUser } from "src/auth/decorator/getUser.decorator"; import { GetUser } from "src/auth/decorator/getUser.decorator";
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard"; import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
import { JwtGuard } from "src/auth/guard/jwt.guard"; import { JwtGuard } from "src/auth/guard/jwt.guard";
import { ConfigService } from "../config/config.service";
import { CreateUserDTO } from "./dto/createUser.dto"; import { CreateUserDTO } from "./dto/createUser.dto";
import { UpdateOwnUserDTO } from "./dto/updateOwnUser.dto"; import { UpdateOwnUserDTO } from "./dto/updateOwnUser.dto";
import { UpdateUserDto } from "./dto/updateUser.dto"; import { UpdateUserDto } from "./dto/updateUser.dto";
import { UserDTO } from "./dto/user.dto"; import { UserDTO } from "./dto/user.dto";
import { UserSevice } from "./user.service"; import { UserSevice } from "./user.service";
import { ConfigService } from "../config/config.service";
@Controller("users") @Controller("users")
export class UserController { export class UserController {
constructor( constructor(
private userService: UserSevice, private userService: UserSevice,
private config: ConfigService, private config: ConfigService
) {} ) {}
// Own user operations // Own user operations
@ -42,17 +43,20 @@ export class UserController {
@UseGuards(JwtGuard) @UseGuards(JwtGuard)
async updateCurrentUser( async updateCurrentUser(
@GetUser() user: User, @GetUser() user: User,
@Body() data: UpdateOwnUserDTO, @Body() data: UpdateOwnUserDTO
) { ) {
return new UserDTO().from(await this.userService.update(user.id, data)); return new UserDTO().from(await this.userService.update(user.id, data));
} }
@Delete("me") @Delete("me")
@HttpCode(204)
@UseGuards(JwtGuard) @UseGuards(JwtGuard)
async deleteCurrentUser( async deleteCurrentUser(
@GetUser() user: User, @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"); const isSecure = this.config.get("general.secureCookies");
response.cookie("access_token", "accessToken", { response.cookie("access_token", "accessToken", {
@ -65,7 +69,6 @@ export class UserController {
maxAge: -1, maxAge: -1,
secure: isSecure, secure: isSecure,
}); });
return new UserDTO().from(await this.userService.delete(user.id));
} }
// Global user operations // Global user operations

View File

@ -2,15 +2,15 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import * as argon from "argon2"; import * as argon from "argon2";
import * as crypto from "crypto"; 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 { EmailService } from "src/email/email.service";
import { PrismaService } from "src/prisma/prisma.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 { FileService } from "../file/file.service";
import { CreateUserDTO } from "./dto/createUser.dto"; import { CreateUserDTO } from "./dto/createUser.dto";
import { UpdateUserDto } from "./dto/updateUser.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() @Injectable()
export class UserSevice { export class UserSevice {
@ -20,7 +20,7 @@ export class UserSevice {
private prisma: PrismaService, private prisma: PrismaService,
private emailService: EmailService, private emailService: EmailService,
private fileService: FileService, private fileService: FileService,
private configService: ConfigService, private configService: ConfigService
) {} ) {}
async list() { async list() {
@ -55,7 +55,7 @@ export class UserSevice {
if (e.code == "P2002") { if (e.code == "P2002") {
const duplicatedField: string = e.meta.target[0]; const duplicatedField: string = e.meta.target[0];
throw new BadRequestException( 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") { if (e.code == "P2002") {
const duplicatedField: string = e.meta.target[0]; const duplicatedField: string = e.meta.target[0];
throw new BadRequestException( 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) 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( 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 } }); return await this.prisma.user.delete({ where: { id } });
@ -98,7 +108,7 @@ export class UserSevice {
async findOrCreateFromLDAP( async findOrCreateFromLDAP(
providedCredentials: AuthSignInDTO, providedCredentials: AuthSignInDTO,
ldapEntry: Entry, ldapEntry: Entry
) { ) {
const fieldNameMemberOf = this.configService.get("ldap.fieldNameMemberOf"); const fieldNameMemberOf = this.configService.get("ldap.fieldNameMemberOf");
const fieldNameEmail = this.configService.get("ldap.fieldNameEmail"); const fieldNameEmail = this.configService.get("ldap.fieldNameEmail");
@ -112,7 +122,7 @@ export class UserSevice {
isAdmin = entryGroups.includes(adminGroup) ?? false; isAdmin = entryGroups.includes(adminGroup) ?? false;
} else { } else {
this.logger.warn( 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 { } else {
this.logger.warn( 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) => { .catch((error) => {
this.logger.warn( 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) => { .then((newUser) => {
this.logger.log( 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; user.email = newUser.email;
}) })
.catch((error) => { .catch((error) => {
this.logger.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") { if (e.code == "P2002") {
const duplicatedField: string = e.meta.target[0]; const duplicatedField: string = e.meta.target[0];
throw new BadRequestException( throw new BadRequestException(
`A user with this ${duplicatedField} already exists`, `A user with this ${duplicatedField} already exists`
); );
} }
} }

View File

@ -56,7 +56,7 @@ const Account = () => {
username: yup username: yup
.string() .string()
.min(3, t("common.error.too-short", { length: 3 })), .min(3, t("common.error.too-short", { length: 3 })),
}), })
), ),
}); });
@ -79,7 +79,7 @@ const Account = () => {
.string() .string()
.min(8, t("common.error.too-short", { length: 8 })) .min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")), .required(t("common.error.field-required")),
}), })
), ),
}); });
@ -93,7 +93,7 @@ const Account = () => {
.string() .string()
.min(8, t("common.error.too-short", { length: 8 })) .min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")), .required(t("common.error.field-required")),
}), })
), ),
}); });
@ -110,7 +110,7 @@ const Account = () => {
.min(6, t("common.error.exact-length", { length: 6 })) .min(6, t("common.error.exact-length", { length: 6 }))
.max(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") }), .matches(/^[0-9]+$/, { message: t("common.error.invalid-number") }),
}), })
), ),
}); });
@ -155,7 +155,7 @@ const Account = () => {
email: values.email, email: values.email,
}) })
.then(() => toast.success(t("account.notify.info.success"))) .then(() => toast.success(t("account.notify.info.success")))
.catch(toast.axiosError), .catch(toast.axiosError)
)} )}
> >
<Stack> <Stack>
@ -193,7 +193,7 @@ const Account = () => {
toast.success(t("account.notify.password.success")); toast.success(t("account.notify.password.success"));
passwordForm.reset(); passwordForm.reset();
}) })
.catch(toast.axiosError), .catch(toast.axiosError)
)} )}
> >
<Stack> <Stack>
@ -265,7 +265,7 @@ const Account = () => {
unlinkOAuth(provider) unlinkOAuth(provider)
.then(() => { .then(() => {
toast.success( toast.success(
t("account.notify.oauth.unlinked.success"), t("account.notify.oauth.unlinked.success")
); );
refreshOAuthStatus(); refreshOAuthStatus();
}) })
@ -281,7 +281,7 @@ const Account = () => {
component="a" component="a"
href={getOAuthUrl( href={getOAuthUrl(
config.get("general.appUrl"), config.get("general.appUrl"),
provider, provider
)} )}
> >
{t("account.card.oauth.link")} {t("account.card.oauth.link")}
@ -324,7 +324,7 @@ const Account = () => {
<Stack> <Stack>
<PasswordInput <PasswordInput
description={t( description={t(
"account.card.security.totp.disable.description", "account.card.security.totp.disable.description"
)} )}
label={t("account.card.password.title")} label={t("account.card.password.title")}
{...disableTotpForm.getInputProps("password")} {...disableTotpForm.getInputProps("password")}
@ -366,7 +366,7 @@ const Account = () => {
<PasswordInput <PasswordInput
label={t("account.card.password.title")} label={t("account.card.password.title")}
description={t( description={t(
"account.card.security.totp.enable.description", "account.card.security.totp.enable.description"
)} )}
{...enableTotpForm.getInputProps("password")} {...enableTotpForm.getInputProps("password")}
/> />
@ -414,8 +414,10 @@ const Account = () => {
}, },
confirmProps: { color: "red" }, confirmProps: { color: "red" },
onConfirm: async () => { onConfirm: async () => {
await userService.removeCurrentUser(); await userService
window.location.reload(); .removeCurrentUser()
.then(window.location.reload)
.catch(toast.axiosError);
}, },
}) })
} }