Files
pleno-vue/src/components/forms/auth/LoginForm.vue
T

285 lines
7.9 KiB
Vue

<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import { useI18n } from 'vue-i18n'
import axios from 'axios'
import {API_URL} from "@/config.js";
import { parseError, clearErrors, addError, getError } from "@/components/request/HandleGlobalError.vue";
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
import TwoFactorVerify from "@/components/forms/auth/TwoFactorVerify.vue";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
const { t } = useI18n();
// Passkey support
const passkeySupported = ref(false);
const isPasskeyLoading = ref(false);
// 2FA state
const requires2FA = ref(false);
const twoFactorToken = ref('');
onMounted(() => {
passkeySupported.value = isPasskeySupported();
});
const customer_number = ref();
const password = ref('');
const error = ref('');
const successMessage = ref('');
const store = useStore();
const router = useRouter();
const resolveLoginRedirectPath = () => {
const redirectQuery = router.currentRoute.value.query.redirect;
const redirectValue = Array.isArray(redirectQuery) ? redirectQuery[0] : redirectQuery;
if (!redirectValue) {
return "/redirect";
}
try {
const redirectUrl = new URL(redirectValue, window.location.origin);
if (redirectUrl.origin !== window.location.origin) {
return "/redirect";
}
return `${redirectUrl.pathname}${redirectUrl.search}${redirectUrl.hash}`;
} catch {
return "/redirect";
}
};
const login = async () => {
try {
const response = await axios.post(API_URL + '/auth/login', {
customer_number: customer_number.value,
password: password.value,
g_recaptcha_response: reCAPTCHA_data.value.enabled ? grecaptcha.getResponse() : null,
});
const data = response.data.data || response.data;
// Check if 2FA is required
if (data['2fa_required'] || data.two_fa_required) {
requires2FA.value = true;
twoFactorToken.value = data['2fa_token'] || data.two_fa_token;
return;
}
// Save the token in the local storage
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', data.token);
// Set the success message
successMessage.value = "Du er nu logget ind!"
// Wait 500ms before reloading the page to show the success message
setTimeout(() => {
window.location.assign(resolveLoginRedirectPath());
}, 500);
// window.location.reload();
} catch (e) {
parseError(e, 'auth');
}
};
const on2FACancel = () => {
requires2FA.value = false;
twoFactorToken.value = '';
password.value = '';
};
const doNothing = () => {
// Do nothing
};
// Passkey login
const loginWithPasskey = async () => {
isPasskeyLoading.value = true;
clearErrors();
try {
// Parse customer_number as integer - API expects integer type
const customerNum = customer_number.value ? parseInt(customer_number.value, 10) : null;
// Get recaptcha token if enabled
const recaptchaToken = reCAPTCHA_data.value.enabled ? grecaptcha.getResponse() : null;
const result = await authenticateWithPasskey('user', customerNum, recaptchaToken);
console.log('Passkey authentication result:', result);
if (result.token) {
clearEdgeGatewayWorkspaceCache();
localStorage.setItem('token', result.token);
successMessage.value = "Du er nu logget ind!";
setTimeout(() => {
router.go(0);
}, 500);
} else {
// No token returned - this shouldn't happen for successful auth
addError('auth', t('passkeys.login_failed'));
}
} catch (e) {
console.error('Passkey login error:', e);
// Check if it's an axios error with response data
if (e.response?.data?.message) {
addError('auth', e.response.data.message);
} else if (e.response?.data?.error) {
addError('auth', e.response.data.error);
} else {
addError('auth', e.message || t('passkeys.login_failed'));
}
} finally {
isPasskeyLoading.value = false;
}
};
const reCAPTCHA_data = ref({
enabled: false,
site_key: '',
});
const ratelimit_data = ref({
enabled: false,
limit: 0,
remaining: 0,
reset: 0,
warning: null,
});
onMounted(async () => {
const response = await axios.get(`${API_URL}/auth/reCAPTCHA/public`);
const data = response.data.data;
console.log(data);
reCAPTCHA_data.value = data.recaptcha;
ratelimit_data.value = data.rate_limit;
// If the reCAPTCHA is enabled, load the reCAPTCHA script
if (reCAPTCHA_data.value.enabled) {
const script = document.createElement('script');
script.src = 'https://www.google.com/recaptcha/api.js';
script.async = true;
script.defer = true;
document.head.appendChild(script);
}
});
</script>
<template>
<!-- 2FA Verification -->
<TwoFactorVerify
v-if="requires2FA"
:two-factor-token="twoFactorToken"
user-type="user"
@cancel="on2FACancel"
/>
<!-- Normal Login Form -->
<form v-else @submit.prevent="doNothing">
<div class="form-field">
<label class="label">{{ $t('auth.customer_number') }}</label>
<input type="text" v-model="customer_number" name="customer_number" data-testid="login-customer-number" />
</div>
<div class="form-field">
<label class="label">{{ $t('auth.password') }}</label>
<input type="password" v-model="password" name="password" data-testid="login-password" />
</div>
<!-- reCaptcha, if enabled -->
<div class="field mt-6" v-if="reCAPTCHA_data.enabled">
<div class="control">
<div class="g-recaptcha" :data-sitekey="reCAPTCHA_data.site_key"></div>
</div>
</div>
<!-- Display the error -->
<div class="field" v-if="getError('auth')">
<p class="help is-danger" id="user_auth_alert_error">
{{ getError('auth') }}
</p>
</div>
<div class="field" v-if="successMessage">
<!-- Success message for login -->
<p class="help is-success" id="user_auth_alert_success">
{{ successMessage }}
</p>
</div>
<div class="form-field">
<label><input type="checkbox" id="remember" name="remember">{{ $t('auth.remember_me') }}</label>
</div>
<router-link :to="{name: 'password-reset'}" class="" id="forgot-password-button">{{ $t('auth.forgot_password') }}</router-link>
<LoadButtonWhileAwait :loadFunction="login" class="submit" id="login-button" data-testid="login-submit">{{ $t('auth.login') }}</LoadButtonWhileAwait>
<router-link :to="{name: 'loginqr'}" class="submit">{{ $t('auth.login_with_qr_code') }}</router-link>
<!-- Passkey login button -->
<button
v-if="passkeySupported"
type="button"
class="submit passkey-btn"
@click="loginWithPasskey"
:disabled="isPasskeyLoading"
id="passkey-login-button"
>
<span class="icon" v-if="!isPasskeyLoading">
<i class="fas fa-fingerprint"></i>
</span>
<span class="icon" v-else>
<i class="fas fa-spinner fa-spin"></i>
</span>
<span>{{ $t('passkeys.login_with_passkey') }}</span>
</button>
</form>
</template>
<style scoped>
form{
display: flex;
flex-direction: column;
gap: 16px;
max-width: 400px;
margin-bottom: 40px;
}
.form-field{
display: flex;
flex-direction: column;
gap: 4px;
}
.form-field label{
color: #13324C;
font-size: 16px;
line-height: 1;
display: flex;
gap: 16px;
}
form input{
border: 1.5px solid #333;
border-radius: 8px;
margin-top: 4px;
font-size: 16px;
line-height: 1;
}
form input:not([type="checkbox"]) {
min-height: 40px;
}
.submit {
width: 100%;
border-radius: 30px;
background-color: #13324C;
border: 0;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
min-height: 43px;
cursor: pointer;
}
.submit:hover {
background-color: #1a4263;
}
.passkey-btn {
gap: 8px;
}
</style>