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