Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve strings and layout around login and security pages #1399

Draft
wants to merge 4 commits into
base: staging
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@
"indexTitle": "Home",
"authorsTitle": "Authors",
"staffTitle": "Staff",
"login": "Login",
"signup": "Signup",
"login": "Log in",
"signup": "Sign up",
"user": {
"profile": "Profile",
"notifications": "Notifications",
Expand Down Expand Up @@ -939,6 +939,23 @@
}
},
"auth": {
"login": {
"main": {
"title": "Log in to Hangar"
},
"sudo": {
"title": "Verify your identity",
"info": "You need to authenticate again to perform this action."
},
"twoFactor": {
"title": "Two-factor authentication",
"info": "Your account uses two-factor authentication. Use one of your saved methods to continue.",
"webauthn": {
"button": "Use passkey",
"genericError": "Passkey authentication failed. Try again, or use another method."
}
}
},
"settings": {
"profile": {
"header": "Profile",
Expand All @@ -955,14 +972,21 @@
},
"security": {
"header": "Security",
"authApp": "Authenticator App",
"authApp": {
"name": "Authenticator App",
"none": "Set up a mobile authenticator app to generate one-time passcodes as a two-factor authentication method.",
"active": "You have set up an authenticator app as a two-factor method."
},
"devices": "Devices",
"button": {
"setupAuthApp": "Setup 2FA via authenticator app",
"setupSecurityKey": "Setup 2FA via security key",
"linkGithub": "Link a GitHub account",
"linkGoogle": "Link a Google account",
"linkMicrosoft": "Link a Microsoft account",
"linkOther": "Link {0} account",
"unlinkAccount": "Unlink {0} account {1}"
"unlinkAccount": "Unlink {0} account {1}",
"unlinkTotp": "Remove authenticator app"
},
"authAppSetup": {
"scan": "Scan the QR code on the right using your favorite authenticator app",
Expand All @@ -974,16 +998,19 @@
"name": "Security Keys",
"keyName": "Name",
"unregister": "Unregister",
"rename": "Rename"
"rename": "Rename",
"registerTitle": "Register a new security key",
"none": "You have no security keys registered. You can add a trusted device to your account as a two-factor authentication method."
},
"backupCodes": {
"name": "Backup Codes",
"info": "If you lose access to your other two-factor methods, you can use a backup code to log in.",
"generateNew": "Generate new codes",
"modal": {
"title": "Confirm backup codes",
"needConfigure": "You need to configure backup codes before you can activate 2fa. Please save these codes securely!",
"confirm": "Confirm that you saved the backup codes by entering one of them below",
"backupCode": "Backup Code"
"title": "Save your backup codes",
"needConfigure": "If you ever lose access to your main two-factor authentication methods, you can use these backup codes to log in. Store these somewhere safe.",
"confirm": "To finish setting up two-factor authentication, confirm that you saved the backup codes by entering one of them below:",
"backupCode": "Enter a backup code"
}
},
"unlinkOAuth": {
Expand Down
31 changes: 25 additions & 6 deletions frontend/src/pages/auth/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ if (privileged) {
loading.value = false;
}

const dialogTitle = computed(() => {
if (privileged) {
return i18n.t("auth.login.sudo.title");
}
if (supportedMethods.value.length > 0) {
return i18n.t("auth.login.twoFactor.title");
}
return i18n.t("auth.login.main.title");
})

const dialogInfo = computed(() => {
if (privileged) {
return i18n.t("auth.login.sudo.info");
}
if (supportedMethods.value.length > 0) {
return i18n.t("auth.login.twoFactor.info");
}
return null;
})

async function loginPassword() {
if (!(await v.value.$validate())) return;
loading.value = true;
Expand Down Expand Up @@ -125,9 +145,9 @@ useHead(useSeo("Login", null, route, null));

<template>
<Card class="w-xl mx-auto max-w-full">
<template #header>{{ privileged ? "Sudo" : "Login" }}</template>
<template #header>{{ dialogTitle }}</template>

<div v-if="privileged" class="mb-2">Please authenticate again to do this action</div>
<div v-if="!!dialogInfo" class="mb-2">{{ dialogInfo }}</div>

<form v-if="supportedMethods.length === 0" class="flex flex-col gap-2">
<InputText
Expand Down Expand Up @@ -185,15 +205,14 @@ useHead(useSeo("Login", null, route, null));
</form>

<form v-if="supportedMethods.length > 0" class="flex flex-col gap-2 hide-last-hr">
<p>Please verify your sign in using one of your second factors</p>
<template v-if="supportedMethods.includes('WEBAUTHN')">
<Button class="w-max" :disabled="loading" @click.prevent="loginWebAuthN">Use WebAuthN</Button>
<Button class="w-max" :disabled="loading" @click.prevent="loginWebAuthN">Use WebAuthn</Button>
<hr />
</template>
<template v-if="supportedMethods.includes('TOTP')">
<div class="flex flex-col gap-2">
<InputText v-model="totpCode" label="Totp code" inputmode="numeric" />
<Button class="w-max" :disabled="loading" @click.prevent="loginTotp">Use totp</Button>
<InputText v-model="totpCode" label="TOTP code" inputmode="numeric" />
<Button class="w-max" :disabled="loading" @click.prevent="loginTotp">Use TOTP</Button>
</div>
<hr />
</template>
Expand Down
22 changes: 18 additions & 4 deletions frontend/src/pages/auth/settings/security.vue
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,9 @@ function closeUnlinkModal() {
<template>
<div v-if="auth.user">
<PageTitle>{{ t("auth.settings.security.header") }}</PageTitle>
<h3 class="text-lg font-bold mb-2">{{ t("auth.settings.security.authApp") }}</h3>
<Button v-if="settings?.hasTotp" :disabled="loading" @click="unlinkTotp">Unlink totp</Button>
<h3 class="text-lg font-bold mb-2">{{ t("auth.settings.security.authApp.name") }}</h3>
<p class="mb-2">{{ settings?.hasTotp ? t("auth.settings.security.authApp.active") : t("auth.settings.security.authApp.none") }}</p>
<Button v-if="settings?.hasTotp" :disabled="loading" @click="unlinkTotp">{{ t("auth.settings.security.button.unlinkTotp") }}</Button>
<Button v-else-if="!totpData" :disabled="loading" @click="setupTotp"> {{ t("auth.settings.security.button.setupAuthApp") }} </Button>
<div v-else class="flex lt-sm:flex-col gap-8">
<div class="flex flex-col gap-2 basis-1/2">
Expand All @@ -280,7 +281,7 @@ function closeUnlinkModal() {
</div>
</div>
<div class="basis-1/2">
<img :src="totpData.qrCode" alt="totp qr code" class="w-60" />
<img :src="totpData.qrCode" alt="QR code for TOTP setup" class="w-60" />
<small>{{ totpData.secret }}</small>
</div>
</div>
Expand Down Expand Up @@ -314,6 +315,8 @@ function closeUnlinkModal() {
</Button>
</Modal>
</ul>
<p v-if="settings?.authenticators.length == 0">{{ t("auth.settings.security.securityKeys.none") }}</p>
<h4 class="font-semibold mt-4 mb-2">{{ t("auth.settings.security.securityKeys.registerTitle") }}</h4>
<div class="my-2">
<InputText
v-model="authenticatorName"
Expand All @@ -325,6 +328,7 @@ function closeUnlinkModal() {

<template v-if="settings?.hasBackupCodes">
<h3 class="text-lg font-bold mt-4 mb-2">{{ t("auth.settings.security.backupCodes.name") }}</h3>
<p class="mb-2">{{ t("auth.settings.security.backupCodes.info") }}</p>
<div v-if="showCodes" class="flex flex-wrap mt-2 mb-2">
<div v-for="code in codes" :key="code.code" class="basis-3/12">
<code>{{ code["used_at"] ? t("general.used") : code.code }}</code>
Expand All @@ -343,6 +347,14 @@ function closeUnlinkModal() {
<IconMdiGithub class="mr-1" />
{{ t("auth.settings.security.button.linkGithub") }}
</template>
<template v-else-if="provider === 'google'">
<IconMdiGoogle class="mr-1" />
{{ t("auth.settings.security.button.linkGoogle") }}
</template>
<template v-else-if="provider === 'microsoft'">
<IconMdiMicrosoft class="mr-1" />
{{ t("auth.settings.security.button.linkMicrosoft") }}
</template>
<template v-else> {{ t("auth.settings.security.button.linkOther", [provider]) }} </template>
</Button>
</div>
Expand Down Expand Up @@ -374,7 +386,7 @@ function closeUnlinkModal() {
<code>{{ code.code }}</code>
</div>
</div>
{{ t("auth.settings.security.backupCodes.modal.confirm") }}
<p class="mb-2">{{ t("auth.settings.security.backupCodes.modal.confirm") }}</p>
<InputText
v-model="backupCodeConfirm"
:label="t('auth.settings.security.backupCodes.modal.backupCode')"
Expand All @@ -383,11 +395,13 @@ function closeUnlinkModal() {
<Button class="mt-2" :disabled="v.$invalid" @click="confirmAndRepeat">{{ t("general.confirm") }}</Button>
</Modal>

<!-- TODO: implement session list
<h3 class="text-lg font-bold mt-4 mb-2">{{ t("auth.settings.security.devices") }}</h3>
<ComingSoon>
last login<br />
on revoke iphone<br />
revoke all
</ComingSoon>
-->
</div>
</template>
Loading