Initial Version
This commit is contained in:
28
core/src/views/PrivateLayout.vue
Normal file
28
core/src/views/PrivateLayout.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import LayoutHeader from '@KTXC/layouts/header/LayoutHeader.vue';
|
||||
import { useLayoutStore } from '@KTXC/stores/layoutStore';
|
||||
import LayoutSystemMenu from '@KTXC/layouts/menus/LayoutSystemMenu.vue';
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-locale-provider>
|
||||
<v-app :class="[layoutStore.miniSidebar ? 'mini-sidebar' : '']">
|
||||
<LayoutHeader />
|
||||
<LayoutSystemMenu />
|
||||
|
||||
<v-main class="page-wrapper">
|
||||
<RouterView />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</v-locale-provider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.v-main.page-wrapper {
|
||||
height: calc(100vh - 64px) !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
</style>
|
||||
26
core/src/views/PublicLayout.vue
Normal file
26
core/src/views/PublicLayout.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import LayoutFooter from '@KTXC/layouts/footer/LayoutFooter.vue';
|
||||
import { useLayoutStore } from '@KTXC/stores/layoutStore';
|
||||
|
||||
const layoutStore = useLayoutStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-locale-provider>
|
||||
<v-app :class="[layoutStore.miniSidebar ? 'mini-sidebar' : '']">
|
||||
<v-main class="page-wrapper">
|
||||
<v-container fluid>
|
||||
<div>
|
||||
<RouterView />
|
||||
</div>
|
||||
</v-container>
|
||||
<v-container fluid class="pt-0">
|
||||
<div>
|
||||
<LayoutFooter />
|
||||
</div>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</v-locale-provider>
|
||||
</template>
|
||||
38
core/src/views/authentication/AuthFooter.vue
Normal file
38
core/src/views/authentication/AuthFooter.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef } from 'vue';
|
||||
|
||||
const footerLink = shallowRef([
|
||||
{
|
||||
title: 'Terms and Conditions'
|
||||
},
|
||||
{
|
||||
title: 'Privacy Policy'
|
||||
},
|
||||
{
|
||||
title: 'CA Privacy Notice'
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
<template>
|
||||
<v-footer class="px-0 pt-2">
|
||||
<v-row justify="center" no-gutters>
|
||||
<v-col cols="12" md="6" class="text-md-left text-center">
|
||||
<p class="text-subtitle-2 text-lightText mb-md-0 mb-4">
|
||||
This site is protected by
|
||||
<a href="/" class="text-primary">Privacy Policy</a>
|
||||
</p>
|
||||
</v-col>
|
||||
<v-col class="d-flex flex-md-row flex-column justify-md-end align-center" cols="12" md="6">
|
||||
<a
|
||||
v-for="(item, i) in footerLink"
|
||||
:key="i"
|
||||
class="mx-md-3 mx-2 mb-md-0 mb-2 text-subtitle-2 text-lightText"
|
||||
href="https://codedthemes.com"
|
||||
target="_blank"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-footer>
|
||||
</template>
|
||||
586
core/src/views/authentication/AuthLogin.vue
Normal file
586
core/src/views/authentication/AuthLogin.vue
Normal file
@@ -0,0 +1,586 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useUserStore } from '@KTXC/stores/userStore';
|
||||
import { authenticationService } from '@KTXC/services/authenticationService';
|
||||
import type { AuthenticationMethod, VerifyResponse } from '@KTXC/types/authenticationTypes';
|
||||
import { Form } from 'vee-validate';
|
||||
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// Login flow phases
|
||||
type LoginPhase = 'identity' | 'method' | 'mfa';
|
||||
|
||||
// Form state
|
||||
const identity = ref('');
|
||||
const authResponse = ref(''); // password, code, etc.
|
||||
const showPassword = ref(false);
|
||||
const rememberMe = ref(false);
|
||||
|
||||
// Auth state
|
||||
const session = ref<string | null>(null);
|
||||
const phase = ref<LoginPhase>('identity');
|
||||
const allMethods = ref<AuthenticationMethod[]>([]);
|
||||
const availableMethods = ref<AuthenticationMethod[]>([]);
|
||||
const selectedMethod = ref<AuthenticationMethod | null>(null);
|
||||
const loading = ref(true);
|
||||
const submitting = ref(false);
|
||||
const ssoLoading = ref<string | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
const challengeSent = ref(false);
|
||||
|
||||
// Separate methods by type
|
||||
const redirectMethods = computed(() =>
|
||||
allMethods.value.filter(m => m.method === 'redirect')
|
||||
);
|
||||
|
||||
const nonRedirectMethods = computed(() =>
|
||||
availableMethods.value.filter(m => m.method !== 'redirect')
|
||||
);
|
||||
|
||||
const hasNonRedirectMethods = computed(() =>
|
||||
allMethods.value.some(m => m.method !== 'redirect')
|
||||
);
|
||||
|
||||
// Computed UI states
|
||||
const showIdentityForm = computed(() => phase.value === 'identity');
|
||||
const showAuthForm = computed(() => phase.value === 'method' && selectedMethod.value !== null);
|
||||
const showMfaForm = computed(() => phase.value === 'mfa');
|
||||
const showSsoButtons = computed(() => redirectMethods.value.length > 0 && phase.value === 'identity');
|
||||
|
||||
// Title based on phase
|
||||
const pageTitle = computed(() => {
|
||||
switch (phase.value) {
|
||||
case 'identity': return 'Login';
|
||||
case 'method': return 'Verify Your Identity';
|
||||
case 'mfa': return 'Additional Verification';
|
||||
default: return 'Login';
|
||||
}
|
||||
});
|
||||
|
||||
// Input label/type based on selected method
|
||||
const authInputLabel = computed(() => {
|
||||
if (!selectedMethod.value) return 'Password';
|
||||
return selectedMethod.value.method === 'credential' ? 'Password' : 'Verification Code';
|
||||
});
|
||||
|
||||
const authInputType = computed(() => {
|
||||
if (!selectedMethod.value) return 'password';
|
||||
return selectedMethod.value.method === 'credential' ? 'password' : 'text';
|
||||
});
|
||||
|
||||
// Validation rules
|
||||
const identityRules = [
|
||||
(v: string) => !!v.trim() || 'Email is required',
|
||||
(v: string) => !/\s/.test(v.trim()) || 'Email must not contain spaces',
|
||||
(v: string) => /.+@.+\..+/.test(v.trim()) || 'Email must be valid'
|
||||
];
|
||||
|
||||
const authResponseRules = [
|
||||
(v: string) => !!v || 'This field is required',
|
||||
];
|
||||
|
||||
// Initialize authentication
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Check for error from SSO callback
|
||||
const errorParam = route.query.error as string;
|
||||
if (errorParam) {
|
||||
error.value = decodeURIComponent(errorParam);
|
||||
}
|
||||
|
||||
// Check for MFA session from redirect
|
||||
const mfaSession = route.query.session as string;
|
||||
if (mfaSession) {
|
||||
session.value = mfaSession;
|
||||
await loadSessionStatus();
|
||||
} else {
|
||||
// Initialize new auth session
|
||||
await initSession();
|
||||
}
|
||||
|
||||
// Auto-trigger SSO if provider param present
|
||||
const providerParam = route.query.provider as string;
|
||||
if (providerParam && phase.value === 'identity') {
|
||||
const provider = allMethods.value.find(m => m.id === providerParam);
|
||||
if (provider && provider.method === 'redirect') {
|
||||
await initiateSsoLogin(provider.id);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message || 'Failed to initialize authentication';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for method selection changes (for challenge-based methods)
|
||||
watch(selectedMethod, async (newMethod) => {
|
||||
if (newMethod && newMethod.method === 'challenge' && !challengeSent.value) {
|
||||
// Initiate challenge for methods that need it (SMS, email, TOTP)
|
||||
await initiateChallenge(newMethod.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize a new session
|
||||
async function initSession() {
|
||||
const result = await authenticationService.start();
|
||||
session.value = result.session;
|
||||
allMethods.value = result.methods;
|
||||
phase.value = 'identity';
|
||||
}
|
||||
|
||||
// Load existing session status (for MFA continuation)
|
||||
async function loadSessionStatus() {
|
||||
if (!session.value) return;
|
||||
|
||||
try {
|
||||
const status = await authenticationService.getStatus(session.value);
|
||||
|
||||
if (status.state === 'secondary_pending') {
|
||||
phase.value = 'mfa';
|
||||
availableMethods.value = status.methods;
|
||||
if (availableMethods.value.length > 0) {
|
||||
selectedMethod.value = availableMethods.value[0];
|
||||
}
|
||||
} else if (status.state === 'identified') {
|
||||
// Session has identity, show method selection
|
||||
phase.value = 'method';
|
||||
availableMethods.value = status.methods;
|
||||
autoSelectMethod();
|
||||
} else {
|
||||
// Session not in expected state, reinitialize
|
||||
await initSession();
|
||||
}
|
||||
} catch (e) {
|
||||
// Session expired or invalid, reinitialize
|
||||
await initSession();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle identity submission (step 1)
|
||||
async function handleIdentitySubmit(_values: any, { setErrors }: any) {
|
||||
if (!session.value) return;
|
||||
|
||||
error.value = null;
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
const result = await authenticationService.identify(session.value, identity.value.trim());
|
||||
|
||||
session.value = result.session;
|
||||
availableMethods.value = result.methods;
|
||||
phase.value = 'method';
|
||||
|
||||
autoSelectMethod();
|
||||
} catch (e: any) {
|
||||
const errorMessage = e.message || 'Failed to continue. Please try again.';
|
||||
setErrors({ apiError: errorMessage });
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-select method if only one available
|
||||
function autoSelectMethod() {
|
||||
const methods = nonRedirectMethods.value;
|
||||
if (methods.length === 1) {
|
||||
selectedMethod.value = methods[0];
|
||||
} else if (methods.length > 1) {
|
||||
// Default to credential if available
|
||||
const credMethod = methods.find(m => m.method === 'credential');
|
||||
selectedMethod.value = credMethod || methods[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle method selection
|
||||
function selectMethod(method: AuthenticationMethod) {
|
||||
selectedMethod.value = method;
|
||||
authResponse.value = '';
|
||||
challengeSent.value = false;
|
||||
}
|
||||
|
||||
// Initiate a challenge (for SMS, email, TOTP)
|
||||
async function initiateChallenge(methodId: string) {
|
||||
if (!session.value) return;
|
||||
|
||||
try {
|
||||
await authenticationService.beginChallenge(session.value, methodId);
|
||||
challengeSent.value = true;
|
||||
} catch (e: any) {
|
||||
// Challenge initiation failed - show generic error
|
||||
error.value = e.message || 'Failed to send verification code.';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle authentication response submission (step 2)
|
||||
async function handleAuthSubmit(_values: any, { setErrors }: any) {
|
||||
if (!session.value || !selectedMethod.value) return;
|
||||
|
||||
error.value = null;
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
const result = await authenticationService.verify(
|
||||
session.value,
|
||||
selectedMethod.value.id,
|
||||
authResponse.value
|
||||
// identity is already in session from identify() step
|
||||
);
|
||||
|
||||
handleVerifyResponse(result);
|
||||
} catch (e: any) {
|
||||
const errorMessage = e.message || 'Verification failed. Please try again.';
|
||||
setErrors({ apiError: errorMessage });
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MFA verification (step 3)
|
||||
async function handleMfaSubmit(_values: any, { setErrors }: any) {
|
||||
if (!session.value || !selectedMethod.value) return;
|
||||
|
||||
error.value = null;
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
const result = await authenticationService.verify(
|
||||
session.value,
|
||||
selectedMethod.value.id,
|
||||
authResponse.value
|
||||
);
|
||||
|
||||
handleVerifyResponse(result);
|
||||
} catch (e: any) {
|
||||
const errorMessage = e.message || 'Verification failed. Please try again.';
|
||||
setErrors({ apiError: errorMessage });
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle verify response
|
||||
function handleVerifyResponse(result: VerifyResponse) {
|
||||
if (result.status === 'success' && result.user) {
|
||||
// Authentication complete
|
||||
userStore.setAuth(result.user);
|
||||
window.location.replace('/');
|
||||
} else if (result.status === 'pending') {
|
||||
// MFA required
|
||||
phase.value = 'mfa';
|
||||
availableMethods.value = result.methods || [];
|
||||
if (result.session) {
|
||||
session.value = result.session;
|
||||
}
|
||||
if (availableMethods.value.length > 0) {
|
||||
selectedMethod.value = availableMethods.value[0];
|
||||
}
|
||||
authResponse.value = '';
|
||||
challengeSent.value = false;
|
||||
} else if (result.error) {
|
||||
error.value = result.error;
|
||||
}
|
||||
}
|
||||
|
||||
// Initiate SSO login
|
||||
async function initiateSsoLogin(methodId: string) {
|
||||
if (!session.value) return;
|
||||
|
||||
ssoLoading.value = methodId;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const result = await authenticationService.beginRedirect(session.value, methodId, '/');
|
||||
window.location.href = result.redirect_url;
|
||||
} catch (e: any) {
|
||||
error.value = e.message || 'Failed to initiate SSO login';
|
||||
ssoLoading.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Go back to identity phase
|
||||
function backToIdentity() {
|
||||
phase.value = 'identity';
|
||||
selectedMethod.value = null;
|
||||
authResponse.value = '';
|
||||
challengeSent.value = false;
|
||||
error.value = null;
|
||||
|
||||
// Reinitialize session
|
||||
initSession();
|
||||
}
|
||||
|
||||
function getMethodIcon(method: AuthenticationMethod): string {
|
||||
if (method.icon) return method.icon;
|
||||
switch (method.method) {
|
||||
case 'credential': return 'mdi-key';
|
||||
case 'challenge': return 'mdi-shield-check';
|
||||
case 'redirect': return 'mdi-login';
|
||||
default: return 'mdi-shield-check';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<h3 class="text-h3 text-center mb-0">{{ pageTitle }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="mt-7 text-center">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<p class="mt-2 text-medium-emphasis">Loading login options...</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Error Alert -->
|
||||
<v-alert v-if="error" type="error" class="mt-4" closable @click:close="error = null">
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
<!-- Phase 1: Identity Input -->
|
||||
<template v-if="showIdentityForm">
|
||||
<!-- Identity Form -->
|
||||
<Form
|
||||
v-if="hasNonRedirectMethods"
|
||||
@submit="handleIdentitySubmit"
|
||||
class="loginForm"
|
||||
:class="{ 'mt-7': !showSsoButtons }"
|
||||
v-slot="{ errors, isSubmitting }"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<v-label>Email Address</v-label>
|
||||
<v-text-field
|
||||
v-model="identity"
|
||||
:rules="identityRules"
|
||||
class="mt-2"
|
||||
required
|
||||
hide-details="auto"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
autocomplete="email"
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="isSubmitting || submitting"
|
||||
block
|
||||
class="mt-5"
|
||||
variant="flat"
|
||||
size="large"
|
||||
type="submit"
|
||||
>
|
||||
Continue
|
||||
</v-btn>
|
||||
|
||||
<!-- SSO Buttons -->
|
||||
<div v-if="showSsoButtons" class="mt-7">
|
||||
<v-btn
|
||||
v-for="method in redirectMethods"
|
||||
:key="method.id"
|
||||
color="secondary"
|
||||
:loading="ssoLoading === method.id"
|
||||
:disabled="ssoLoading !== null"
|
||||
block
|
||||
class="mb-3"
|
||||
variant="outlined"
|
||||
size="large"
|
||||
@click="initiateSsoLogin(method.id)"
|
||||
>
|
||||
<v-icon v-if="method.icon" start>{{ method.icon }}</v-icon>
|
||||
{{ method.label }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="errors.apiError" class="mt-2">
|
||||
<v-alert color="error">{{ errors.apiError }}</v-alert>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<!-- No providers -->
|
||||
<div v-if="!hasNonRedirectMethods && !showSsoButtons" class="mt-7 text-center">
|
||||
<v-alert type="warning">
|
||||
No login methods are currently available. Please contact your administrator.
|
||||
</v-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Phase 2: Method Selection & Authentication -->
|
||||
<template v-else-if="showAuthForm && !showMfaForm">
|
||||
<p class="text-body-1 text-medium-emphasis mt-4 mb-2">
|
||||
Signing in as <strong>{{ identity }}</strong>
|
||||
</p>
|
||||
|
||||
<!-- Method Selector (if multiple methods) -->
|
||||
<div v-if="nonRedirectMethods.length > 1" class="mb-4">
|
||||
<v-label class="mb-2">Choose verification method</v-label>
|
||||
<v-btn-toggle
|
||||
:model-value="selectedMethod?.id"
|
||||
mandatory
|
||||
variant="outlined"
|
||||
divided
|
||||
class="w-100"
|
||||
>
|
||||
<v-btn
|
||||
v-for="method in nonRedirectMethods"
|
||||
:key="method.id"
|
||||
:value="method.id"
|
||||
@click="selectMethod(method)"
|
||||
class="flex-grow-1"
|
||||
>
|
||||
<v-icon start>{{ getMethodIcon(method) }}</v-icon>
|
||||
{{ method.label }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
<!-- Auth Response Form -->
|
||||
<Form
|
||||
@submit="handleAuthSubmit"
|
||||
class="loginForm mt-4"
|
||||
v-slot="{ errors, isSubmitting }"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<v-label>{{ authInputLabel }}</v-label>
|
||||
<v-text-field
|
||||
v-model="authResponse"
|
||||
:rules="authResponseRules"
|
||||
:type="authInputType === 'password' && !showPassword ? 'password' : 'text'"
|
||||
class="mt-2"
|
||||
required
|
||||
hide-details="auto"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
:autocomplete="selectedMethod?.method === 'credential' ? 'current-password' : 'one-time-code'"
|
||||
:inputmode="selectedMethod?.method !== 'credential' ? 'numeric' : undefined"
|
||||
autofocus
|
||||
>
|
||||
<template v-if="authInputType === 'password'" v-slot:append-inner>
|
||||
<v-btn
|
||||
variant="text"
|
||||
density="compact"
|
||||
:icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
@click="showPassword = !showPassword"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedMethod?.method === 'credential'" class="d-flex align-center mt-4 mb-7 mb-sm-0">
|
||||
<v-checkbox
|
||||
v-model="rememberMe"
|
||||
label="Keep me logged in"
|
||||
color="primary"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
<div class="ml-auto">
|
||||
<router-link to="/forgot-password" class="text-primary text-decoration-none">
|
||||
Forgot Password?
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="isSubmitting || submitting"
|
||||
block
|
||||
class="mt-5"
|
||||
variant="flat"
|
||||
size="large"
|
||||
type="submit"
|
||||
>
|
||||
{{ selectedMethod?.method === 'credential' ? 'Login' : 'Verify' }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
variant="text"
|
||||
block
|
||||
class="mt-3"
|
||||
@click="backToIdentity"
|
||||
>
|
||||
Use different account
|
||||
</v-btn>
|
||||
|
||||
<div v-if="errors.apiError" class="mt-2">
|
||||
<v-alert color="error">{{ errors.apiError }}</v-alert>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<!-- Phase 3: MFA Verification -->
|
||||
<template v-if="showMfaForm">
|
||||
<p class="text-body-1 text-medium-emphasis mt-4 mb-6">
|
||||
Additional verification is required. Please enter the code from your {{ selectedMethod?.label || 'authenticator' }}.
|
||||
</p>
|
||||
|
||||
<!-- MFA Method Selector (if multiple) -->
|
||||
<v-select
|
||||
v-if="availableMethods.length > 1"
|
||||
v-model="selectedMethod"
|
||||
:items="availableMethods"
|
||||
item-title="label"
|
||||
item-value="id"
|
||||
return-object
|
||||
label="Verification Method"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
></v-select>
|
||||
|
||||
<Form @submit="handleMfaSubmit" v-slot="{ errors, isSubmitting }">
|
||||
<div class="mb-6">
|
||||
<v-label>Verification Code</v-label>
|
||||
<v-text-field
|
||||
v-model="authResponse"
|
||||
:rules="authResponseRules"
|
||||
type="text"
|
||||
class="mt-2"
|
||||
required
|
||||
hide-details="auto"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
autocomplete="one-time-code"
|
||||
inputmode="numeric"
|
||||
autofocus
|
||||
></v-text-field>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="isSubmitting || submitting"
|
||||
block
|
||||
class="mt-5"
|
||||
variant="flat"
|
||||
size="large"
|
||||
type="submit"
|
||||
>
|
||||
Verify
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
variant="text"
|
||||
block
|
||||
class="mt-3"
|
||||
@click="backToIdentity"
|
||||
>
|
||||
Back to Login
|
||||
</v-btn>
|
||||
|
||||
<div v-if="errors.apiError" class="mt-2">
|
||||
<v-alert color="error">{{ errors.apiError }}</v-alert>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.loginForm {
|
||||
.v-text-field .v-field--active input {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
core/src/views/authentication/LoginPage.vue
Normal file
53
core/src/views/authentication/LoginPage.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import Logo from '@KTXC/layouts/logo/LogoDark.vue';
|
||||
import AuthLogin from './AuthLogin.vue';
|
||||
import AuthFooter from './AuthFooter.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row class="bg-containerBg position-relative" no-gutters>
|
||||
<v-col cols="12">
|
||||
<div class="pt-6 pl-6">
|
||||
<Logo />
|
||||
</div>
|
||||
</v-col>
|
||||
<!---Login Part-->
|
||||
<v-col cols="12" lg="12" class="d-flex align-center">
|
||||
<v-container>
|
||||
<div class="d-flex align-center justify-center" style="min-height: calc(100vh - 148px)">
|
||||
<v-row justify="center">
|
||||
<v-col cols="12" md="12">
|
||||
<v-card elevation="0" class="loginBox">
|
||||
<v-card elevation="24">
|
||||
<v-card-text class="pa-sm-10 pa-6">
|
||||
<!---Login Form-->
|
||||
<AuthLogin />
|
||||
<!---Login Form-->
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-col>
|
||||
<!---Login Part-->
|
||||
<v-col cols="12">
|
||||
<v-container class="pt-0 pb-6">
|
||||
<AuthFooter />
|
||||
</v-container>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.loginBox {
|
||||
max-width: 475px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.blur-logo {
|
||||
position: absolute;
|
||||
filter: blur(18px);
|
||||
bottom: 0;
|
||||
transform: inherit;
|
||||
}
|
||||
</style>
|
||||
50
core/src/views/pages/maintenance/error/Error404Page.vue
Normal file
50
core/src/views/pages/maintenance/error/Error404Page.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-row no-gutters class="overflow-hidden bg-containerBg" style="min-height: 100vh">
|
||||
<v-col class="d-flex align-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="CardMediaWrapper">
|
||||
<img src="@KTXC/assets/images/maintenance/Error404.png" alt="404" />
|
||||
<div class="CardMediaBuild">
|
||||
<img src="@KTXC/assets/images/maintenance/TwoCone.png" alt="grid" class="w-100" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-h1 mt-16">Page Not Found</h1>
|
||||
<p class="text-h6 text-lightText">The page you are looking was moved, removed, <br />renamed, or might never exist!</p>
|
||||
<v-btn variant="flat" color="primary" class="mt-2" to="/"> Back To Home</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.CardMediaWrapper {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
> img {
|
||||
@media (min-width: 0px) {
|
||||
width: 250px;
|
||||
height: 130px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
width: 590px;
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.CardMediaBuild {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
@media (min-width: 0px) {
|
||||
width: 130px;
|
||||
height: 115px;
|
||||
right: -14%;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
width: 390px;
|
||||
height: 330px;
|
||||
right: -60%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
28
core/src/views/pages/maintenance/error/Error500Page.vue
Normal file
28
core/src/views/pages/maintenance/error/Error500Page.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-row no-gutters class="overflow-hidden bg-containerBg" style="min-height: 100vh">
|
||||
<v-col class="d-flex align-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="errorMedia">
|
||||
<img src="@/assets/images/maintenance/Error500.png" alt="404" />
|
||||
</div>
|
||||
<h1 class="text-h1 mt-2 mb-1">Internal Server Error</h1>
|
||||
<p class="text-caption text-lightText">Server error 500. we fixing the problem. please try <br />again at a later stage.</p>
|
||||
<v-btn variant="flat" color="primary" class="mt-4" to="/"> Back To Home</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.errorMedia {
|
||||
> img {
|
||||
@media (min-width: 0px) {
|
||||
width: 350px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
width: 396px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user