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:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 } },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user