mirror of https://github.com/wg-easy/wg-easy
7 changed files with 285 additions and 150 deletions
@ -0,0 +1,106 @@ |
|||
<template> |
|||
<div> |
|||
<form class="flex flex-col gap-5" @submit.prevent="submit"> |
|||
<BaseInput |
|||
v-model="totp" |
|||
type="text" |
|||
name="totp" |
|||
:placeholder="$t('general.2faCode')" |
|||
autocomplete="one-time-code" |
|||
inputmode="numeric" |
|||
maxlength="6" |
|||
pattern="\d{6}" |
|||
autofocus |
|||
/> |
|||
|
|||
<button |
|||
class="rounded bg-red-800 py-2 text-sm text-white shadow transition hover:bg-red-700 disabled:cursor-not-allowed disabled:bg-gray-200 dark:bg-red-800 dark:text-white dark:hover:bg-red-700 disabled:dark:bg-neutral-800" |
|||
:disabled="!totp || authenticating" |
|||
> |
|||
<IconsLoading v-if="authenticating" class="mx-auto w-5 animate-spin" /> |
|||
<span v-else>{{ $t('general.continue') }}</span> |
|||
</button> |
|||
|
|||
<button |
|||
type="button" |
|||
class="rounded border-2 border-gray-100 py-2 text-sm text-gray-700 transition hover:border-red-800 hover:bg-red-800 hover:text-white dark:border-neutral-600 dark:text-neutral-200" |
|||
@click="cancel" |
|||
> |
|||
{{ $t('dialog.cancel') }} |
|||
</button> |
|||
</form> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const toast = useToast(); |
|||
const { t } = useI18n(); |
|||
const loginStore = useLoginStore(); |
|||
|
|||
const authenticating = ref(false); |
|||
const totp = ref<string>(''); |
|||
|
|||
if (!loginStore.hasPendingLogin) { |
|||
await navigateTo('/login'); |
|||
} |
|||
|
|||
const _submit = useSubmit( |
|||
(data) => |
|||
$fetch('/api/auth/password', { |
|||
method: 'post', |
|||
body: data, |
|||
}), |
|||
{ |
|||
revert: async (success, data) => { |
|||
if (success) { |
|||
if (data?.status === 'success') { |
|||
loginStore.clearPendingLogin(); |
|||
await navigateTo('/'); |
|||
} else if (data?.status === 'INVALID_TOTP_CODE') { |
|||
authenticating.value = false; |
|||
totp.value = ''; |
|||
toast.showToast({ |
|||
title: t('general.2fa'), |
|||
message: t('login.2faWrong'), |
|||
type: 'error', |
|||
}); |
|||
return; |
|||
} else if (data?.status === 'TOTP_REQUIRED') { |
|||
authenticating.value = false; |
|||
totp.value = ''; |
|||
toast.showToast({ |
|||
title: t('general.2fa'), |
|||
message: t('login.2faRequired'), |
|||
type: 'error', |
|||
}); |
|||
return; |
|||
} |
|||
} |
|||
authenticating.value = false; |
|||
}, |
|||
noSuccessToast: true, |
|||
} |
|||
); |
|||
|
|||
async function submit() { |
|||
const challenge = loginStore.pendingChallenge; |
|||
|
|||
if (!challenge || !totp.value || authenticating.value) return; |
|||
|
|||
authenticating.value = true; |
|||
|
|||
if (challenge.type === 'password') { |
|||
return _submit({ |
|||
username: challenge.username, |
|||
password: challenge.password, |
|||
remember: challenge.remember, |
|||
totpCode: totp.value, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
async function cancel() { |
|||
loginStore.clearPendingLogin(); |
|||
await navigateTo('/login'); |
|||
} |
|||
</script> |
|||
@ -0,0 +1,130 @@ |
|||
<template> |
|||
<div> |
|||
<div |
|||
v-if="authMethods && authMethods.oauthEnabled" |
|||
class="flex flex-col gap-5" |
|||
> |
|||
<UiLoginOauthButton |
|||
v-for="(info, provider) in authMethods.providers" |
|||
:key="provider" |
|||
:provider="provider" |
|||
:info="info" |
|||
/> |
|||
|
|||
<!-- Divider --> |
|||
<div |
|||
v-if="authMethods.oauthEnabled && !authMethods.passwordDisabled" |
|||
class="flex items-center gap-2" |
|||
> |
|||
<div class="h-px flex-1 bg-gray-300 dark:bg-neutral-600"></div> |
|||
<span class="text-xs text-gray-500 dark:text-neutral-400"> |
|||
{{ $t('login.or') }} |
|||
</span> |
|||
<div class="h-px flex-1 bg-gray-300 dark:bg-neutral-600"></div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Classic Login Form --> |
|||
<form |
|||
v-if="!authMethods?.passwordDisabled" |
|||
class="flex flex-col gap-5" |
|||
@submit.prevent="submit" |
|||
> |
|||
<BaseInput |
|||
v-model="username" |
|||
type="text" |
|||
:placeholder="$t('general.username')" |
|||
autocomplete="username" |
|||
autofocus |
|||
name="username" |
|||
/> |
|||
|
|||
<BaseInput |
|||
v-model="password" |
|||
type="password" |
|||
name="password" |
|||
:placeholder="$t('general.password')" |
|||
autocomplete="current-password" |
|||
/> |
|||
|
|||
<label |
|||
class="flex gap-2 whitespace-nowrap" |
|||
:title="$t('login.rememberMeDesc')" |
|||
> |
|||
<BaseSwitch v-model="remember" /> |
|||
<span class="text-sm">{{ $t('login.rememberMe') }}</span> |
|||
</label> |
|||
|
|||
<button |
|||
class="rounded bg-red-800 py-2 text-sm text-white shadow transition hover:bg-red-700 disabled:cursor-not-allowed disabled:bg-gray-200 dark:bg-red-800 dark:text-white dark:hover:bg-red-700 disabled:dark:bg-neutral-800" |
|||
:disabled="!password || !username" |
|||
> |
|||
<IconsLoading v-if="authenticating" class="mx-auto w-5 animate-spin" /> |
|||
<span v-else>{{ $t('login.signIn') }}</span> |
|||
</button> |
|||
</form> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
const toast = useToast(); |
|||
const { t } = useI18n(); |
|||
const loginStore = useLoginStore(); |
|||
|
|||
const authenticating = ref(false); |
|||
const remember = ref(false); |
|||
const username = ref<string>(''); |
|||
const password = ref<string>(''); |
|||
|
|||
const { data: authMethods } = await useFetch('/api/auth/methods'); |
|||
|
|||
const _submit = useSubmit( |
|||
(data) => |
|||
$fetch('/api/auth/password', { |
|||
method: 'post', |
|||
body: data, |
|||
}), |
|||
{ |
|||
revert: async (success, data) => { |
|||
if (success) { |
|||
if (data?.status === 'success') { |
|||
loginStore.clearPendingLogin(); |
|||
await navigateTo('/'); |
|||
} else if (data?.status === 'TOTP_REQUIRED') { |
|||
authenticating.value = false; |
|||
loginStore.setPendingPasswordLogin( |
|||
username.value, |
|||
password.value, |
|||
remember.value |
|||
); |
|||
await navigateTo('/login/2fa'); |
|||
return; |
|||
} else if (data?.status === 'INVALID_TOTP_CODE') { |
|||
authenticating.value = false; |
|||
toast.showToast({ |
|||
title: t('general.2fa'), |
|||
message: t('login.2faWrong'), |
|||
type: 'error', |
|||
}); |
|||
return; |
|||
} |
|||
} |
|||
authenticating.value = false; |
|||
password.value = ''; |
|||
}, |
|||
noSuccessToast: true, |
|||
} |
|||
); |
|||
|
|||
async function submit() { |
|||
if (!username.value || !password.value || authenticating.value) return; |
|||
|
|||
authenticating.value = true; |
|||
|
|||
return _submit({ |
|||
username: username.value, |
|||
password: password.value, |
|||
remember: remember.value, |
|||
}); |
|||
} |
|||
</script> |
|||
@ -0,0 +1,36 @@ |
|||
type PendingLoginChallenge = { |
|||
type: 'password'; |
|||
username: string; |
|||
password: string; |
|||
remember: boolean; |
|||
}; |
|||
|
|||
export const useLoginStore = defineStore('Login', () => { |
|||
const pendingChallenge = ref<PendingLoginChallenge | null>(null); |
|||
|
|||
const hasPendingLogin = computed(() => pendingChallenge.value !== null); |
|||
|
|||
function setPendingPasswordLogin( |
|||
username: string, |
|||
password: string, |
|||
remember: boolean |
|||
) { |
|||
pendingChallenge.value = { |
|||
type: 'password', |
|||
username, |
|||
password, |
|||
remember, |
|||
}; |
|||
} |
|||
|
|||
function clearPendingLogin() { |
|||
pendingChallenge.value = null; |
|||
} |
|||
|
|||
return { |
|||
pendingChallenge, |
|||
hasPendingLogin, |
|||
setPendingPasswordLogin, |
|||
clearPendingLogin, |
|||
}; |
|||
}); |
|||
Loading…
Reference in new issue