Initial Version

This commit is contained in:
root
2025-12-21 10:09:54 -05:00
commit 4ae6befc7b
422 changed files with 47225 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>