feat(auth): Allow to hide username / password login form when OAuth is enabled (#518)

* 🚀 Feature: Allow to hide username / password login form when OAuth is enabled

* Hide “Sign in” password form
* Disable routes related to password authentication
* Change styling of OAuth provider buttons
* Open OAuth page in same tab
* Fix consistent usage of informal language in de-DE locale

Fixes #489

Signed-off-by: Marvin A. Ruder <signed@mruder.dev>

* fix: order of new config variables

---------

Signed-off-by: Marvin A. Ruder <signed@mruder.dev>
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Marvin A. Ruder
2024-07-07 23:08:14 +02:00
committed by GitHub
parent 9d9cc7b4ab
commit e1a68f75f7
5 changed files with 89 additions and 45 deletions

View File

@@ -71,7 +71,6 @@ const configVariables: ConfigVariables = {
enableShareEmailRecipients: { enableShareEmailRecipients: {
type: "boolean", type: "boolean",
defaultValue: "false", defaultValue: "false",
secret: false, secret: false,
}, },
shareRecipientsSubject: { shareRecipientsSubject: {
@@ -148,6 +147,11 @@ const configVariables: ConfigVariables = {
type: "boolean", type: "boolean",
defaultValue: "true", defaultValue: "true",
}, },
"disablePassword": {
type: "boolean",
defaultValue: "false",
secret: false,
},
"github-enabled": { "github-enabled": {
type: "boolean", type: "boolean",
defaultValue: "false", defaultValue: "false",
@@ -229,7 +233,7 @@ const configVariables: ConfigVariables = {
defaultValue: "", defaultValue: "",
obscured: true, obscured: true,
}, },
} },
}; };
type ConfigVariables = { type ConfigVariables = {
@@ -281,12 +285,15 @@ async function seedConfigVariables() {
async function migrateConfigVariables() { async function migrateConfigVariables() {
const existingConfigVariables = await prisma.config.findMany(); const existingConfigVariables = await prisma.config.findMany();
const orderMap: { [category: string]: number } = {};
for (const existingConfigVariable of existingConfigVariables) { for (const existingConfigVariable of existingConfigVariables) {
const configVariable = const configVariable =
configVariables[existingConfigVariable.category]?.[ configVariables[existingConfigVariable.category]?.[
existingConfigVariable.name existingConfigVariable.name
]; ];
// Delete the config variable if it doesn't exist in the seed
if (!configVariable) { if (!configVariable) {
await prisma.config.delete({ await prisma.config.delete({
where: { where: {
@@ -297,15 +304,11 @@ async function migrateConfigVariables() {
}, },
}); });
// Update the config variable if the metadata changed // Update the config variable if it exists in the seed
} else if ( } else {
JSON.stringify({ const variableOrder = Object.keys(
...configVariable, configVariables[existingConfigVariable.category]
name: existingConfigVariable.name, ).indexOf(existingConfigVariable.name);
category: existingConfigVariable.category,
value: existingConfigVariable.value,
}) != JSON.stringify(existingConfigVariable)
) {
await prisma.config.update({ await prisma.config.update({
where: { where: {
name_category: { name_category: {
@@ -318,8 +321,10 @@ async function migrateConfigVariables() {
name: existingConfigVariable.name, name: existingConfigVariable.name,
category: existingConfigVariable.category, category: existingConfigVariable.category,
value: existingConfigVariable.value, value: existingConfigVariable.value,
order: variableOrder,
}, },
}); });
orderMap[existingConfigVariable.category] = variableOrder + 1;
} }
} }
} }

View File

@@ -61,6 +61,9 @@ export class AuthService {
if (!dto.email && !dto.username) if (!dto.email && !dto.username)
throw new BadRequestException("Email or username is required"); throw new BadRequestException("Email or username is required");
if (this.config.get("oauth.disablePassword"))
throw new ForbiddenException("Password sign in is disabled");
const user = await this.prisma.user.findFirst({ const user = await this.prisma.user.findFirst({
where: { where: {
OR: [{ email: dto.email }, { username: dto.username }], OR: [{ email: dto.email }, { username: dto.username }],
@@ -94,6 +97,9 @@ export class AuthService {
} }
async requestResetPassword(email: string) { async requestResetPassword(email: string) {
if (this.config.get("oauth.disablePassword"))
throw new ForbiddenException("Password sign in is disabled");
const user = await this.prisma.user.findFirst({ const user = await this.prisma.user.findFirst({
where: { email }, where: { email },
include: { resetPasswordToken: true }, include: { resetPasswordToken: true },
@@ -119,6 +125,9 @@ export class AuthService {
} }
async resetPassword(token: string, newPassword: string) { async resetPassword(token: string, newPassword: string) {
if (this.config.get("oauth.disablePassword"))
throw new ForbiddenException("Password sign in is disabled");
const user = await this.prisma.user.findFirst({ const user = await this.prisma.user.findFirst({
where: { resetPasswordToken: { token } }, where: { resetPasswordToken: { token } },
}); });

View File

@@ -28,6 +28,19 @@ import toast from "../../utils/toast.util";
import { safeRedirectPath } from "../../utils/router.util"; import { safeRedirectPath } from "../../utils/router.util";
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
signInWith: {
fontWeight: 500,
"&:before": {
content: "''",
flex: 1,
display: "block",
},
"&:after": {
content: "''",
flex: 1,
display: "block",
},
},
or: { or: {
"&:before": { "&:before": {
content: "''", content: "''",
@@ -128,6 +141,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
</Text> </Text>
)} )}
<Paper withBorder shadow="md" p={30} mt={30} radius="md"> <Paper withBorder shadow="md" p={30} mt={30} radius="md">
{config.get("oauth.disablePassword") || (
<form <form
onSubmit={form.onSubmit((values) => { onSubmit={form.onSubmit((values) => {
signIn(values.emailOrUsername, values.password); signIn(values.emailOrUsername, values.password);
@@ -155,22 +169,30 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
<FormattedMessage id="signin.button.submit" /> <FormattedMessage id="signin.button.submit" />
</Button> </Button>
</form> </form>
)}
{oauth.length > 0 && ( {oauth.length > 0 && (
<Stack mt="xl"> <Stack mt={config.get("oauth.disablePassword") ? undefined : "xl"}>
{config.get("oauth.disablePassword") ? (
<Group align="center" className={classes.signInWith}>
<Text>{t("signIn.oauth.signInWith")}</Text>
</Group>
) : (
<Group align="center" className={classes.or}> <Group align="center" className={classes.or}>
<Text>{t("signIn.oauth.or")}</Text> <Text>{t("signIn.oauth.or")}</Text>
</Group> </Group>
)}
<Group position="center"> <Group position="center">
{oauth.map((provider) => ( {oauth.map((provider) => (
<Button <Button
key={provider} key={provider}
component="a" component="a"
target="_blank"
title={t(`signIn.oauth.${provider}`)} title={t(`signIn.oauth.${provider}`)}
href={getOAuthUrl(config.get("general.appUrl"), provider)} href={getOAuthUrl(config.get("general.appUrl"), provider)}
variant="light" variant="light"
fullWidth
> >
{getOAuthIcon(provider)} {getOAuthIcon(provider)}
{"\u2002" + t(`signIn.oauth.${provider}`)}
</Button> </Button>
))} ))}
</Group> </Group>

View File

@@ -34,6 +34,7 @@ export default {
"signIn.notify.totp-required.title": "Zwei-Faktor-Authentifizierung benötigt", "signIn.notify.totp-required.title": "Zwei-Faktor-Authentifizierung benötigt",
"signIn.notify.totp-required.description": "Bitte füge deinen Zwei-Faktor-Authentifizierungscode ein", "signIn.notify.totp-required.description": "Bitte füge deinen Zwei-Faktor-Authentifizierungscode ein",
"signIn.oauth.or": "ODER", "signIn.oauth.or": "ODER",
"signIn.oauth.signInWith": "Anmelden mit",
"signIn.oauth.github": "GitHub", "signIn.oauth.github": "GitHub",
"signIn.oauth.google": "Google", "signIn.oauth.google": "Google",
"signIn.oauth.microsoft": "Microsoft", "signIn.oauth.microsoft": "Microsoft",
@@ -348,6 +349,9 @@ export default {
"admin.config.oauth.allow-registration.description": "Benutzern erlauben, sich über Soziale Netzwerke zu registrieren", "admin.config.oauth.allow-registration.description": "Benutzern erlauben, sich über Soziale Netzwerke zu registrieren",
"admin.config.oauth.ignore-totp": "TOTP ignorieren", "admin.config.oauth.ignore-totp": "TOTP ignorieren",
"admin.config.oauth.ignore-totp.description": "Gibt an, ob TOTP ignoriert werden soll, wenn sich der Benutzer über Soziale Netzwerke anmeldet", "admin.config.oauth.ignore-totp.description": "Gibt an, ob TOTP ignoriert werden soll, wenn sich der Benutzer über Soziale Netzwerke anmeldet",
"admin.config.oauth.disable-password": "Anmelden mit Passwort deaktivieren",
"admin.config.oauth.disable-password.description":
"Deaktiviert das Anmelden mit Passwort\nStelle vor Aktivierung dieser Konfiguration sicher, dass ein OAuth-Provider korrekt konfiguriert ist, um nicht ausgesperrt zu werden.",
"admin.config.oauth.github-enabled": "GitHub", "admin.config.oauth.github-enabled": "GitHub",
"admin.config.oauth.github-enabled.description": "GitHub Anmeldung erlaubt", "admin.config.oauth.github-enabled.description": "GitHub Anmeldung erlaubt",
"admin.config.oauth.github-client-id": "GitHub Client-ID", "admin.config.oauth.github-client-id": "GitHub Client-ID",
@@ -401,7 +405,7 @@ export default {
"error.msg.no_email": "Kann die E-Mail-Adresse von dem Konto {0} nicht abrufen.", "error.msg.no_email": "Kann die E-Mail-Adresse von dem Konto {0} nicht abrufen.",
"error.msg.already_linked": "Das Konto {0} ist bereits mit einem anderen Konto verknüpft.", "error.msg.already_linked": "Das Konto {0} ist bereits mit einem anderen Konto verknüpft.",
"error.msg.not_linked": "Das Konto {0} wurde noch nicht mit einem Konto verknüpft.", "error.msg.not_linked": "Das Konto {0} wurde noch nicht mit einem Konto verknüpft.",
"error.msg.unverified_account": "Dieses Konto {0} wurde noch nicht verifiziert, bitte versuchen Sie es nach der Verifikation erneut.", "error.msg.unverified_account": "Dieses Konto {0} wurde noch nicht verifiziert, bitte versuche es nach der Verifikation erneut.",
"error.msg.discord_guild_permission_denied": "Du bist nicht berechtigt, Dich anzumelden.", "error.msg.discord_guild_permission_denied": "Du bist nicht berechtigt, Dich anzumelden.",
"error.msg.cannot_get_user_info": "Deine Benutzerinformationen können nicht von diesem Konto {0} abgerufen werden.", "error.msg.cannot_get_user_info": "Deine Benutzerinformationen können nicht von diesem Konto {0} abgerufen werden.",
"error.param.provider_github": "GitHub", "error.param.provider_github": "GitHub",

View File

@@ -44,6 +44,7 @@ export default {
"signIn.notify.totp-required.description": "signIn.notify.totp-required.description":
"Please enter your two-factor authentication code", "Please enter your two-factor authentication code",
"signIn.oauth.or": "OR", "signIn.oauth.or": "OR",
"signIn.oauth.signInWith": "Sign in with",
"signIn.oauth.github": "GitHub", "signIn.oauth.github": "GitHub",
"signIn.oauth.google": "Google", "signIn.oauth.google": "Google",
"signIn.oauth.microsoft": "Microsoft", "signIn.oauth.microsoft": "Microsoft",
@@ -479,6 +480,9 @@ export default {
"admin.config.oauth.ignore-totp": "Ignore TOTP", "admin.config.oauth.ignore-totp": "Ignore TOTP",
"admin.config.oauth.ignore-totp.description": "admin.config.oauth.ignore-totp.description":
"Whether to ignore TOTP when user using social login", "Whether to ignore TOTP when user using social login",
"admin.config.oauth.disable-password": "Disable password login",
"admin.config.oauth.disable-password.description":
"Whether to disable password login\nMake sure that an OAuth provider is properly configured before activating this configuration to avoid being locked out.",
"admin.config.oauth.github-enabled": "GitHub", "admin.config.oauth.github-enabled": "GitHub",
"admin.config.oauth.github-enabled.description": "admin.config.oauth.github-enabled.description":
"Whether GitHub login is enabled", "Whether GitHub login is enabled",