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:
parent
4ce64206be
commit
e1a5d19544
@ -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
|
||||||
|
@ -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`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user