first init
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
export { authPublicRoutes } from './routes'
|
||||
export { authMenu } from './menu'
|
||||
export {
|
||||
login,
|
||||
logout,
|
||||
register,
|
||||
verifyEmail,
|
||||
resendVerificationEmail,
|
||||
fetchCurrentUser,
|
||||
getAuthErrorMessage,
|
||||
getRegisterErrorMessage,
|
||||
getVerifyEmailErrorMessage,
|
||||
resolvePostLoginRoute,
|
||||
resolvePostAuthRoute,
|
||||
isAccountPending,
|
||||
isLoginVerificationRequired,
|
||||
} from './services/auth.service'
|
||||
export type {
|
||||
LoginCredentials,
|
||||
LoginResponse,
|
||||
RegisterCredentials,
|
||||
RegisterResponse,
|
||||
VerifyEmailPayload,
|
||||
VerifyEmailResponse,
|
||||
SessionResponse,
|
||||
AuthUser,
|
||||
AuthRole,
|
||||
} from './types/auth.types'
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Menu } from '@/core/types/menu'
|
||||
|
||||
export const authMenu: Menu[] = [
|
||||
{
|
||||
icon: 'CircleGauge',
|
||||
route_name: 'login',
|
||||
title: 'Login',
|
||||
},
|
||||
{
|
||||
icon: 'CircleGauge',
|
||||
route_name: 'register',
|
||||
title: 'Register',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,118 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||
import { logout, resolvePostAuthRoute } from '@/modules/auth'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import logoUrl from '@/assets/images/logo-kopkb.svg'
|
||||
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const loggingOut = ref(false)
|
||||
const checkingStatus = ref(false)
|
||||
|
||||
let statusPollInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const checkActivationStatus = async () => {
|
||||
checkingStatus.value = true
|
||||
|
||||
try {
|
||||
await authStore.fetchSession()
|
||||
|
||||
if (authStore.isAccountActive) {
|
||||
await router.replace(resolvePostAuthRoute(authStore.user))
|
||||
}
|
||||
} finally {
|
||||
checkingStatus.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
loggingOut.value = true
|
||||
|
||||
try {
|
||||
await logout()
|
||||
authStore.clearSession()
|
||||
await router.push({ name: 'login' })
|
||||
} finally {
|
||||
loggingOut.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
statusPollInterval = setInterval(checkActivationStatus, 30000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (statusPollInterval) {
|
||||
clearInterval(statusPollInterval)
|
||||
}
|
||||
})
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[
|
||||
'relative min-h-screen lg:overflow-hidden bg-primary bg-noise xl:bg-background xl:bg-none',
|
||||
'before:hidden before:xl:block before:content-[\'\'] before:w-[57%] before:mt-[-28%] before:mb-[-16%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:rotate-6 before:bg-primary/95 before:bg-noise before:rounded-[35%]',
|
||||
'after:hidden after:xl:block after:content-[\'\'] after:w-[57%] after:mt-[-28%] after:mb-[-16%] after:ml-[-12%] after:absolute after:inset-y-0 after:left-0 after:transform after:rotate-6 after:border after:bg-accent after:bg-cover after:blur-xl after:rounded-[35%] after:border-primary',
|
||||
]">
|
||||
<div :class="[
|
||||
'p-3 sm:px-8 relative min-h-screen',
|
||||
'before:hidden before:xl:block before:w-[57%] before:mt-[-20%] before:mb-[-13%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:-rotate-6 before:bg-primary/40 before:bg-noise before:border before:border-primary/50 before:opacity-60 before:rounded-[20%]',
|
||||
]">
|
||||
<div class="container relative z-10 mx-auto sm:px-20">
|
||||
<div class="block grid-cols-2 gap-4 xl:grid">
|
||||
<div class="hidden min-h-screen flex-col xl:flex">
|
||||
<a class="flex items-center pt-10" href="">
|
||||
<img class="w-6" :src="logoUrl" alt="logo-RAJD" />
|
||||
<span class="ml-3 text-xl font-medium text-white">
|
||||
{{ appName }} {{ appVersion }}
|
||||
</span>
|
||||
</a>
|
||||
<div class="my-auto">
|
||||
<img class="-mt-16 w-1/2" :src="illustrationUrl" alt="logo-RAJD" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-10 flex min-h-screen py-5 xl:my-0 xl:min-h-0 xl:h-auto xl:py-0">
|
||||
<Box raised="double"
|
||||
class="mx-auto my-auto w-full px-5 py-8 sm:w-3/4 sm:px-8 lg:w-2/4 xl:ml-24 xl:w-auto xl:p-0 xl:before:hidden xl:after:hidden xl:shadow-none xl:border-none xl:bg-none">
|
||||
<h2 class="text-center text-2xl font-semibold xl:text-left xl:text-3xl">
|
||||
Akaun Menunggu Pengaktifan
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm opacity-70 xl:text-left">
|
||||
Halo{{ authStore.userName ? `, ${authStore.userName}` : '' }}.
|
||||
</p>
|
||||
|
||||
<AlertRoot class="mt-8" variant="primary">
|
||||
<AlertTitle>E-mel disahkan</AlertTitle>
|
||||
<AlertDescription>
|
||||
Akaun anda sedang menunggu pengaktifan daripada pentadbir sistem. Anda akan dapat
|
||||
mengakses sistem selepas akaun diaktifkan.
|
||||
</AlertDescription>
|
||||
</AlertRoot>
|
||||
|
||||
<div class="mt-8 flex flex-col gap-4">
|
||||
<Button class="box w-full px-4 py-5" variant="primary" type="button" :disabled="checkingStatus"
|
||||
@click="checkActivationStatus">
|
||||
{{ checkingStatus ? 'Checking...' : 'Semak Status' }}
|
||||
</Button>
|
||||
<Button class="box w-full px-4 py-5" look="outline" type="button" :disabled="loggingOut"
|
||||
@click="handleLogout">
|
||||
{{ loggingOut ? 'Logging out...' : 'Log Keluar' }}
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { CheckboxRoot, CheckboxControl, CheckboxLabel } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||
import { PasswordInput } from '@/components/ui/password-input'
|
||||
import {
|
||||
getAuthErrorMessage,
|
||||
login,
|
||||
resolvePostAuthRoute,
|
||||
} from '@/modules/auth'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const remember = ref(false)
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const handleLogin = async () => {
|
||||
errorMessage.value = ''
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await login({
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
remember: remember.value,
|
||||
})
|
||||
|
||||
const loginData = response.data
|
||||
|
||||
if ('requires_email_verification' in loginData) {
|
||||
await router.push({
|
||||
name: 'verify-email',
|
||||
query: { email: loginData.email },
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authStore.setSession(loginData.user, response.active_role ?? null, response.can_switch_role ?? false)
|
||||
await router.push(resolvePostAuthRoute(loginData.user, response.redirect_path))
|
||||
} catch (error) {
|
||||
errorMessage.value = getAuthErrorMessage(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[
|
||||
'relative h-screen lg:overflow-hidden bg-primary bg-noise xl:bg-background xl:bg-none',
|
||||
'before:hidden before:xl:block before:content-[\'\'] before:w-[57%] before:mt-[-28%] before:mb-[-16%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:rotate-6 before:bg-primary/95 before:bg-noise before:rounded-[35%]',
|
||||
'after:hidden after:xl:block after:content-[\'\'] after:w-[57%] after:mt-[-28%] after:mb-[-16%] after:ml-[-12%] after:absolute after:inset-y-0 after:left-0 after:transform after:rotate-6 after:border after:bg-accent after:bg-cover after:blur-xl after:rounded-[35%] after:border-primary',
|
||||
]">
|
||||
<div :class="[
|
||||
'p-3 sm:px-8 relative h-full',
|
||||
'before:hidden before:xl:block before:w-[57%] before:mt-[-20%] before:mb-[-13%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:-rotate-6 before:bg-primary/40 before:bg-noise before:border before:border-primary/50 before:opacity-60 before:rounded-[20%]',
|
||||
]">
|
||||
<div class="container relative z-10 mx-auto sm:px-20">
|
||||
<div class="block grid-cols-2 gap-4 xl:grid">
|
||||
<!-- BEGIN: Login Info -->
|
||||
<div class="hidden min-h-screen flex-col xl:flex">
|
||||
<div class="my-auto">
|
||||
<img class="-mt-16 w-1/2" :src="illustrationUrl" alt="logo-RAJD" />
|
||||
<div class="mt-10 text-4xl font-medium leading-tight text-white">
|
||||
Selamat Datang
|
||||
</div>
|
||||
<div class="mt-5 text-lg text-white opacity-60">
|
||||
Sistem Rejimen Askar Jurutera Diraja (SUTERA)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END: Login Info -->
|
||||
<!-- BEGIN: Login Form -->
|
||||
<div class="my-10 flex h-screen py-5 xl:my-0 xl:h-auto xl:py-0">
|
||||
<Box raised="double"
|
||||
class="mx-auto my-auto w-full px-5 py-8 sm:w-3/4 sm:px-8 lg:w-2/4 xl:ml-24 xl:w-auto xl:p-0 xl:before:hidden xl:after:hidden xl:shadow-none xl:border-none xl:bg-none">
|
||||
<h2 class="text-center text-2xl font-semibold xl:text-left xl:text-3xl">Log Masuk</h2>
|
||||
<div class="mt-2 text-center opacity-70 xl:hidden">
|
||||
Selamat Datang
|
||||
</div>
|
||||
|
||||
<AlertRoot v-if="errorMessage" class="mt-6" variant="danger">
|
||||
<AlertTitle>Login failed</AlertTitle>
|
||||
<AlertDescription>{{ errorMessage }}</AlertDescription>
|
||||
</AlertRoot>
|
||||
|
||||
<form class="mt-8 flex flex-col gap-5" @submit.prevent="handleLogin">
|
||||
<Input v-model="email" class="box block min-w-full px-5 py-6 xl:min-w-md" type="email"
|
||||
placeholder="Email" autocomplete="email" required />
|
||||
<PasswordInput v-model="password" class="box block min-w-full px-5 py-6 xl:min-w-md"
|
||||
placeholder="Password" autocomplete="current-password" required />
|
||||
<div class="flex text-xs sm:text-sm">
|
||||
<div class="mr-auto flex-row items-center">
|
||||
<CheckboxRoot :checked="remember" @checked-change="({ checked }) => (remember = checked === true)">
|
||||
<CheckboxControl />
|
||||
<CheckboxLabel>Ingat saya</CheckboxLabel>
|
||||
</CheckboxRoot>
|
||||
</div>
|
||||
<a class="opacity-70" href="">Lupa Password?</a>
|
||||
</div>
|
||||
<div class="mt-5 text-center xl:mt-10 xl:text-left">
|
||||
<Button class="login-button box w-full px-4 py-5" variant="primary" type="submit" :disabled="loading">
|
||||
{{ loading ? 'Log masuk...' : 'Log masuk' }}
|
||||
</Button>
|
||||
<Button class="box mt-4 w-full px-4 py-5" look="outline" type="button"
|
||||
@click="router.push({ name: 'register' })">
|
||||
Daftar
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Box>
|
||||
</div>
|
||||
<!-- END: Login Form -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,128 @@
|
||||
<script lang="ts" setup>
|
||||
import logoUrl from '@/assets/images/logo.svg'
|
||||
import illustrationUrl from '@/assets/images/illustration.svg'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: 'pengenalan',
|
||||
title: '1. Pengenalan',
|
||||
placeholder: 'Selamat datang ke SUTERA 3.0. Kami amat menghargai kepercayaan yang anda berikan untuk mengendalikan maklumat peribadi anda. Dasar Privasi ini digubal untuk membantu anda memahami bagaimana kami mengumpul, menggunakan, mendedahkan, dan melindungi data peribadi anda selaras dengan Akta Perlindungan Data Peribadi 2010 (PDPA) dan piawaian keselamatan global.\n\nDasar ini terpakai kepada semua TDM, dan mana-mana pihak yang mengakses atau menggunakan perkhidmatan, laman web, dan aplikasi kami.'
|
||||
},
|
||||
{
|
||||
id: 'data-dikumpul',
|
||||
title: '2. Data yang Dikumpul',
|
||||
placeholder:
|
||||
'Kami mengumpul beberapa jenis maklumat termasuk:\n- Maklumat Pengenalan Peribadi: Nama penuh, alamat e-mel, dan nombor telefon.\n- Data Teknikal: Alamat IP, jenis pelayar, dan log aktiviti sistem.\n- Data Transaksi: Sejarah pembelian dan maklumat langganan perkhidmatan.'
|
||||
},
|
||||
{
|
||||
id: 'tujuan-penggunaan',
|
||||
title: '3. Tujuan Penggunaan Data',
|
||||
placeholder: 'Data anda digunakan untuk:\n- Operasi Sistem: Menguruskan akaun dan menyediakan perkhidmatan.\n- Pengesahan: Menjamin keselamatan akses pengguna.\n- Komunikasi: Menghantar notis pentadbiran dan sokongan pelanggan.\n- Analitik: Menambah baik prestasi dan kualiti platform.'
|
||||
},
|
||||
{
|
||||
id: 'perkongsian-data',
|
||||
title: '4. Perkongsian & Pendedahan Data',
|
||||
placeholder: 'Kami tidak menjual data anda. Pendedahan hanya dilakukan kepada:\n- Pembekal Perkhidmatan: Rakan kongsi teknologi untuk pengehosan dan pembayaran.\n- Pematuhan Undang-undang: Jika dikehendaki oleh pihak berkuasa atau perintah mahkamah.'
|
||||
},
|
||||
{
|
||||
id: 'keselamatan',
|
||||
title: '5. Keselamatan Data',
|
||||
placeholder: 'Kami melaksanakan langkah keselamatan teknikal seperti:\n- Penyulitan (Encryption): Menggunakan SSL/TLS untuk perlindungan data.\n- Kawalan Akses: Menghadkan akses data kepada kakitangan yang dibenarkan sahaja.\n- Audit Berkala: Menjalankan semakan keselamatan sistem secara berterusan.'
|
||||
},
|
||||
{
|
||||
id: 'hak-pengguna',
|
||||
title: '6. Hak Pengguna',
|
||||
placeholder: 'Anda mempunyai hak untuk:\n- Akses & Pembetulan: Melihat dan mengemas kini maklumat peribadi anda.\n- Pemadaman: Meminta pemadaman akaun atau data dalam keadaan tertentu.\n- Sekatan: Mengehadkan pemprosesan data anda.'
|
||||
},
|
||||
{
|
||||
id: 'penyimpanan',
|
||||
title: '7. Tempoh Penyimpanan',
|
||||
placeholder: 'Data disimpan hanya selama diperlukan untuk tujuan perniagaan atau undang-undang. Selepas tempoh tersebut, data akan dipadamkan sepenuhnya atau dianonimkan.'
|
||||
},
|
||||
{
|
||||
id: 'kuki',
|
||||
title: '8. Kuki & Teknologi Penjejakan',
|
||||
placeholder: 'Kami menggunakan kuki(cookies) dan token sesi untuk mengingati tetapan anda dan meningkatkan pengalaman pengguna. Anda boleh menguruskan pilihan kuki melalui tetapan pelayar anda.'
|
||||
},
|
||||
{
|
||||
id: 'perubahan',
|
||||
title: '9. Perubahan Dasar',
|
||||
placeholder: 'Dasar ini mungkin dikemas kini dari semasa ke semasa. Sebarang perubahan besar akan dimaklumkan melalui emel atau notis di platform kami sebelum tarikh kuat kuasa baru.'
|
||||
},
|
||||
{
|
||||
|
||||
id: 'hubungi',
|
||||
title: '10. Hubungi Kami',
|
||||
placeholder:
|
||||
'Jika anda ada pertanyaan, komen atau permintaan tentang dasar ini, hubungi kami di\n- KAPT MOHAMMAD EFANDY BIN JAFFARI (effandy.jaffari@army.mil.my)'
|
||||
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[
|
||||
'relative min-h-screen bg-primary bg-noise xl:bg-background xl:bg-none',
|
||||
'before:hidden before:xl:block before:content-[\'\'] before:w-[57%] before:mt-[-28%] before:mb-[-16%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:rotate-6 before:bg-primary/95 before:bg-noise before:rounded-[35%]',
|
||||
'after:hidden after:xl:block after:content-[\'\'] after:w-[57%] after:mt-[-28%] after:mb-[-16%] after:ml-[-12%] after:absolute after:inset-y-0 after:left-0 after:transform after:rotate-6 after:border after:bg-accent after:bg-cover after:blur-xl after:rounded-[35%] after:border-primary',
|
||||
]">
|
||||
<div :class="[
|
||||
'p-3 sm:px-8 relative h-full',
|
||||
'before:hidden before:xl:block before:w-[57%] before:mt-[-20%] before:mb-[-13%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:-rotate-6 before:bg-primary/40 before:bg-noise before:border before:border-primary/50 before:opacity-60 before:rounded-[20%]',
|
||||
]">
|
||||
<div class="container relative z-10 mx-auto sm:px-20 py-10">
|
||||
<div class="mb-8 flex items-center justify-between gap-4">
|
||||
<RouterLink class="flex items-center gap-3" to="/login">
|
||||
<img class="w-6" :src="logoUrl" :alt="`${appName} logo`" />
|
||||
<span class="text-base font-medium text-white">
|
||||
{{ appName }} {{ appVersion }}
|
||||
</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<Box raised="double" class="px-6 py-8 sm:px-10">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h1 class="text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||
Dasar Privasi
|
||||
</h1>
|
||||
<p class="text-sm opacity-80">
|
||||
Dikemas kini terakhir: <span class="font-medium">2 Jun 2026</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid gap-8 lg:grid-cols-[260px_1fr]">
|
||||
<aside class="lg:sticky lg:top-6 lg:self-start">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide opacity-70">
|
||||
Isi Kandungan
|
||||
</div>
|
||||
<ul class="mt-3 flex flex-col gap-2">
|
||||
<li v-for="s in sections" :key="s.id">
|
||||
<a class="text-sm opacity-90 hover:opacity-100 hover:underline" :href="`#${s.id}`">
|
||||
{{ s.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="flex flex-col gap-8">
|
||||
<section v-for="s in sections" :key="s.id" :id="s.id" class="scroll-mt-6">
|
||||
<h2 class="text-lg font-semibold sm:text-xl">
|
||||
{{ s.title }}
|
||||
</h2>
|
||||
<p class="mt-2 whitespace-pre-line leading-relaxed opacity-80">
|
||||
{{ s.placeholder }}
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,138 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { CheckboxRoot, CheckboxControl, CheckboxLabel } from '@/components/ui/checkbox'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||
import { PasswordInput } from '@/components/ui/password-input'
|
||||
import { getRegisterErrorMessage, register } from '@/modules/auth'
|
||||
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const name = ref('')
|
||||
const email = ref('')
|
||||
const icNumber = ref('')
|
||||
const password = ref('')
|
||||
const passwordConfirmation = ref('')
|
||||
const termsAccepted = ref(false)
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!termsAccepted.value) {
|
||||
errorMessage.value = 'Sila bersetuju dengan Dasar Privasi dan Terma dan Syarat.'
|
||||
return
|
||||
}
|
||||
|
||||
errorMessage.value = ''
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await register({
|
||||
name: name.value,
|
||||
email: email.value,
|
||||
ic_number: icNumber.value,
|
||||
password: password.value,
|
||||
password_confirmation: passwordConfirmation.value,
|
||||
})
|
||||
|
||||
await router.push({
|
||||
name: 'verify-email',
|
||||
query: { email: response.data.email },
|
||||
})
|
||||
} catch (error) {
|
||||
errorMessage.value = getRegisterErrorMessage(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[
|
||||
'relative h-screen lg:overflow-hidden bg-primary bg-noise xl:bg-background xl:bg-none',
|
||||
'before:hidden before:xl:block before:content-[\'\'] before:w-[57%] before:mt-[-28%] before:mb-[-16%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:rotate-6 before:bg-primary/95 before:bg-noise before:rounded-[35%]',
|
||||
'after:hidden after:xl:block after:content-[\'\'] after:w-[57%] after:mt-[-28%] after:mb-[-16%] after:ml-[-12%] after:absolute after:inset-y-0 after:left-0 after:transform after:rotate-6 after:border after:bg-accent after:bg-cover after:blur-xl after:rounded-[35%] after:border-primary',
|
||||
]">
|
||||
<div :class="[
|
||||
'p-3 sm:px-8 relative h-full',
|
||||
'before:hidden before:xl:block before:w-[57%] before:mt-[-20%] before:mb-[-13%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:-rotate-6 before:bg-primary/40 before:bg-noise before:border before:border-primary/50 before:opacity-60 before:rounded-[20%]',
|
||||
]">
|
||||
<div class="container relative z-10 mx-auto sm:px-20">
|
||||
<div class="block grid-cols-2 gap-4 xl:grid">
|
||||
<div class="hidden min-h-screen flex-col xl:flex">
|
||||
<div class="my-auto">
|
||||
<img class="-mt-16 w-1/2" :src="illustrationUrl" alt="logo-RAJD" />
|
||||
<div class="mt-10 text-4xl font-medium leading-tight text-white">
|
||||
Selamat Datang
|
||||
</div>
|
||||
<div class="mt-5 text-lg text-white opacity-60">
|
||||
Sistem Rejimen Askar Jurutera Diraja (SUTERA)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-10 flex h-screen py-5 xl:my-0 xl:h-auto xl:py-0">
|
||||
<Box raised="double"
|
||||
class="mx-auto my-auto w-full px-5 py-8 sm:w-3/4 sm:px-8 lg:w-2/4 xl:ml-24 xl:w-auto xl:p-0 xl:before:hidden xl:after:hidden xl:shadow-none xl:border-none xl:bg-none">
|
||||
<h2 class="text-center text-2xl font-semibold xl:text-left xl:text-3xl">Daftar Akaun</h2>
|
||||
<div class="mt-2 text-center opacity-70 xl:hidden">
|
||||
Daftar Akaun
|
||||
</div>
|
||||
|
||||
<AlertRoot v-if="errorMessage" class="mt-6" variant="danger">
|
||||
<AlertTitle>Daftar gagal</AlertTitle>
|
||||
<AlertDescription>{{ errorMessage }}</AlertDescription>
|
||||
</AlertRoot>
|
||||
|
||||
<form class="mt-8 flex flex-col gap-5" @submit.prevent="handleRegister">
|
||||
<Input v-model="name" class="box block min-w-full px-5 py-6 xl:min-w-md" type="text"
|
||||
placeholder="Nama Penuh" autocomplete="name" required />
|
||||
<Input v-model="email" class="box block min-w-full px-5 py-6 xl:min-w-md" type="email"
|
||||
placeholder="Email" autocomplete="email" required />
|
||||
<Input v-model="icNumber" class="box block min-w-full px-5 py-6 xl:min-w-md" type="text"
|
||||
placeholder="No. Kad Pengenalan" required />
|
||||
<PasswordInput v-model="password" class="box block min-w-full px-5 py-6 xl:min-w-md" type="password"
|
||||
placeholder="Password" autocomplete="new-password" minlength="8" required />
|
||||
<PasswordInput v-model="passwordConfirmation" class="box block min-w-full px-5 py-6 xl:min-w-md"
|
||||
placeholder="Sahkan Password" autocomplete="new-password" minlength="8" required />
|
||||
|
||||
<div class="flex text-xs sm:text-sm">
|
||||
<CheckboxRoot :checked="termsAccepted"
|
||||
@checked-change="({ checked }) => (termsAccepted = checked === true)">
|
||||
<CheckboxControl />
|
||||
<CheckboxLabel>
|
||||
Dengan mendaftar, anda bersetuju dengan
|
||||
<RouterLink class="text-primary ml-1" to="/privacy-policy">
|
||||
Dasar Privasi
|
||||
</RouterLink>
|
||||
&
|
||||
<RouterLink class="text-primary ml-1" to="/terms">
|
||||
Terma dan Syarat
|
||||
</RouterLink>
|
||||
.
|
||||
</CheckboxLabel>
|
||||
</CheckboxRoot>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 text-center xl:mt-10 xl:text-left">
|
||||
<Button class="box w-full px-4 py-5" variant="primary" type="submit"
|
||||
:disabled="loading || !termsAccepted">
|
||||
{{ loading ? 'Mendaftar...' : 'Daftar' }}
|
||||
</Button>
|
||||
<Button class="box mt-4 w-full px-4 py-5" look="outline" type="button"
|
||||
@click="router.push({ name: 'login' })">
|
||||
Log masuk
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,139 @@
|
||||
<script lang="ts" setup>
|
||||
import logoUrl from '@/assets/images/logo.svg'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: 'pengenalan',
|
||||
title: '1. Pengenalan',
|
||||
placeholder: `
|
||||
Terma dan Syarat ini mengawal penggunaan platform, laman web, dan perkhidmatan yang disediakan oleh Kementerian Pertahanan Malaysia (MINDEF). Dengan mengakses atau menggunakan perkhidmatan kami, anda dianggap telah membaca, memahami, dan bersetuju untuk terikat dengan terma ini. Jika anda tidak bersetuju dengan mana-mana bahagian terma ini, sila hentikan penggunaan perkhidmatan kami.
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'akaun',
|
||||
title: '2. Akaun & Kelayakan',
|
||||
placeholder: `
|
||||
Anda mesti berumur sekurang-kurangnya 18 tahun dan merupakan tentera. Anda bertanggungjawab memastikan maklumat akaun sentiasa tepat dan terkini. Anda juga bertanggungjawab menjaga kerahsiaan kata laluan serta segala aktiviti yang berlaku di bawah akaun anda.
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'penggunaan',
|
||||
title: '3. Penggunaan Yang Dibenarkan',
|
||||
placeholder: `
|
||||
Anda bersetuju menggunakan perkhidmatan hanya untuk tujuan yang sah dan mematuhi semua undang-undang yang berkuat kuasa. Anda tidak dibenarkan menyalahgunakan sistem, memuat naik kandungan yang menyalahi undang-undang, menyebarkan perisian berbahaya, atau mengganggu operasi platform. Sebarang pelanggaran boleh menyebabkan akaun anda digantung atau ditamatkan.
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'pembayaran',
|
||||
title: '4. Pembayaran & Caj (jika berkenaan)',
|
||||
placeholder: `
|
||||
Sesetengah perkhidmatan mungkin tertakluk kepada bayaran yang dinyatakan semasa proses langganan atau pembelian. Semua bayaran hendaklah dibuat melalui kaedah pembayaran yang diterima oleh platform. Bayaran yang telah dibuat tidak akan dikembalikan kecuali dinyatakan sebaliknya. Harga boleh berubah dari semasa ke semasa dengan notis yang munasabah.
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'kandungan',
|
||||
title: '5. Kandungan & Hak Milik',
|
||||
placeholder: `
|
||||
Semua kandungan, reka bentuk, logo, teks, grafik, dan bahan lain yang terdapat pada platform ini adalah hak milik Kementerian Pertahanan Malaysia (MINDEF) atau pemberi lesennya. Anda diberikan lesen terhad untuk menggunakan kandungan tersebut bagi tujuan penggunaan peribadi dan bukan komersial sahaja. Sebarang penyalinan, pengubahsuaian, atau pengedaran tanpa kebenaran bertulis adalah dilarang.
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'had-liabiliti',
|
||||
title: '6. Had Liabiliti',
|
||||
placeholder: `
|
||||
Perkhidmatan disediakan atas dasar "seadanya" dan "sebagaimana tersedia". Kami tidak menjamin bahawa perkhidmatan akan sentiasa bebas daripada gangguan atau ralat. Setakat yang dibenarkan oleh undang-undang, kami tidak bertanggungjawab terhadap sebarang kerugian langsung, tidak langsung, sampingan, atau berbangkit akibat penggunaan atau ketidakupayaan menggunakan perkhidmatan.
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'penamatan',
|
||||
title: '7. Penamatan',
|
||||
placeholder: `
|
||||
Kami berhak menggantung atau menamatkan akses anda kepada perkhidmatan pada bila-bila masa sekiranya berlaku pelanggaran terma ini atau atas sebab keselamatan. Selepas penamatan, hak anda untuk menggunakan perkhidmatan akan tamat serta-merta dan akses kepada data tertentu mungkin tidak lagi tersedia.
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'perubahan',
|
||||
title: '8. Perubahan Terma',
|
||||
placeholder: `
|
||||
Kami boleh mengemas kini atau meminda Terma dan Syarat ini dari semasa ke semasa. Sebarang perubahan penting akan dimaklumkan melalui laman web, aplikasi, atau e-mel berdaftar anda. Penggunaan berterusan perkhidmatan selepas tarikh kuat kuasa perubahan dianggap sebagai penerimaan terhadap terma yang dikemas kini.
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'hubungi',
|
||||
title: '9. Hubungi Kami',
|
||||
placeholder: `
|
||||
Sekiranya anda mempunyai sebarang pertanyaan berkaitan Terma dan Syarat ini, sila hubungi:
|
||||
|
||||
Nama: KAPT MOHAMMAD EFANDY BIN JAFFARI
|
||||
E-mel: effandy.jaffari@army.mil.my
|
||||
`,
|
||||
},
|
||||
] as const;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[
|
||||
'relative min-h-screen bg-primary bg-noise xl:bg-background xl:bg-none',
|
||||
'before:hidden before:xl:block before:content-[\'\'] before:w-[57%] before:mt-[-28%] before:mb-[-16%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:rotate-6 before:bg-primary/95 before:bg-noise before:rounded-[35%]',
|
||||
'after:hidden after:xl:block after:content-[\'\'] after:w-[57%] before:mt-[-28%] after:mb-[-16%] after:ml-[-12%] after:absolute after:inset-y-0 after:left-0 after:transform after:rotate-6 after:border after:bg-accent after:bg-cover after:blur-xl after:rounded-[35%] after:border-primary',
|
||||
]">
|
||||
<div :class="[
|
||||
'p-3 sm:px-8 relative h-full',
|
||||
'before:hidden before:xl:block before:w-[57%] before:mt-[-20%] before:mb-[-13%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:-rotate-6 before:bg-primary/40 before:bg-noise before:border before:border-primary/50 before:opacity-60 before:rounded-[20%]',
|
||||
]">
|
||||
<div class="container relative z-10 mx-auto sm:px-20 py-10">
|
||||
<div class="mb-8 flex items-center justify-between gap-4">
|
||||
<RouterLink class="flex items-center gap-3" to="/login">
|
||||
<img class="w-6" :src="logoUrl" :alt="`${appName} logo`" />
|
||||
<span class="text-base font-medium text-white">
|
||||
{{ appName }} {{ appVersion }}
|
||||
</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<Box raised="double" class="px-6 py-8 sm:px-10">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h1 class="text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||
Terma dan Syarat
|
||||
</h1>
|
||||
<p class="text-sm opacity-80">
|
||||
Dikemas kini terakhir: <span class="font-medium">2 Jun 2026</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid gap-8 lg:grid-cols-[260px_1fr]">
|
||||
<aside class="lg:sticky lg:top-6 lg:self-start">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide opacity-70">
|
||||
Isi Kandungan
|
||||
</div>
|
||||
<ul class="mt-3 flex flex-col gap-2">
|
||||
<li v-for="s in sections" :key="s.id">
|
||||
<a class="text-sm opacity-90 hover:opacity-100 hover:underline" :href="`#${s.id}`">
|
||||
{{ s.title }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="flex flex-col gap-8">
|
||||
<section v-for="s in sections" :key="s.id" :id="s.id" class="scroll-mt-6">
|
||||
<h2 class="text-lg font-semibold sm:text-xl">
|
||||
{{ s.title }}
|
||||
</h2>
|
||||
<p class="mt-2 whitespace-pre-line leading-relaxed opacity-80">
|
||||
{{ s.placeholder }}
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,164 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
|
||||
import {
|
||||
getVerifyEmailErrorMessage,
|
||||
resendVerificationEmail,
|
||||
resolvePostAuthRoute,
|
||||
verifyEmail,
|
||||
} from '@/modules/auth'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import logoUrl from '@/assets/images/logo-kopkb.svg'
|
||||
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const email = ref('')
|
||||
const otp = ref('')
|
||||
const loading = ref(false)
|
||||
const resendLoading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const resendMessage = ref('')
|
||||
|
||||
const canSubmit = computed(() => email.value.length > 0 && otp.value.length === 6)
|
||||
|
||||
onMounted(() => {
|
||||
const queryEmail = route.query.email
|
||||
if (typeof queryEmail === 'string' && queryEmail) {
|
||||
email.value = queryEmail
|
||||
}
|
||||
})
|
||||
|
||||
const handleVerify = async () => {
|
||||
errorMessage.value = ''
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await verifyEmail({
|
||||
email: email.value,
|
||||
otp: otp.value,
|
||||
})
|
||||
|
||||
authStore.setSession(response.data.user, response.active_role, response.can_switch_role)
|
||||
await router.push(resolvePostAuthRoute(response.data.user))
|
||||
} catch (error) {
|
||||
errorMessage.value = getVerifyEmailErrorMessage(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleResend = async () => {
|
||||
if (!email.value) {
|
||||
errorMessage.value = 'Sila masukkan alamat e-mel.'
|
||||
return
|
||||
}
|
||||
|
||||
errorMessage.value = ''
|
||||
resendMessage.value = ''
|
||||
resendLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await resendVerificationEmail(email.value)
|
||||
resendMessage.value = response.message
|
||||
} catch (error) {
|
||||
errorMessage.value = getVerifyEmailErrorMessage(error)
|
||||
} finally {
|
||||
resendLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const onOtpInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
otp.value = target.value.replace(/\D/g, '').slice(0, 6)
|
||||
}
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[
|
||||
'relative h-screen lg:overflow-hidden bg-primary bg-noise xl:bg-background xl:bg-none',
|
||||
'before:hidden before:xl:block before:content-[\'\'] before:w-[57%] before:mt-[-28%] before:mb-[-16%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:rotate-6 before:bg-primary/95 before:bg-noise before:rounded-[35%]',
|
||||
'after:hidden after:xl:block after:content-[\'\'] after:w-[57%] after:mt-[-28%] after:mb-[-16%] after:ml-[-12%] after:absolute after:inset-y-0 after:left-0 after:transform after:rotate-6 after:border after:bg-accent after:bg-cover after:blur-xl after:rounded-[35%] after:border-primary',
|
||||
]">
|
||||
<div :class="[
|
||||
'p-3 sm:px-8 relative h-full',
|
||||
'before:hidden before:xl:block before:w-[57%] before:mt-[-20%] before:mb-[-13%] before:ml-[-12%] before:absolute before:inset-y-0 before:left-0 before:transform before:-rotate-6 before:bg-primary/40 before:bg-noise before:border before:border-primary/50 before:opacity-60 before:rounded-[20%]',
|
||||
]">
|
||||
<div class="container relative z-10 mx-auto sm:px-20">
|
||||
<div class="block grid-cols-2 gap-4 xl:grid">
|
||||
<div class="hidden min-h-screen flex-col xl:flex">
|
||||
<a class="flex items-center pt-10" href="">
|
||||
<img class="w-6" :src="logoUrl" alt="logo-RAJD" />
|
||||
<span class="ml-3 text-xl font-medium text-white">
|
||||
{{ appName }} {{ appVersion }}
|
||||
</span>
|
||||
</a>
|
||||
<div class="my-auto">
|
||||
<img class="-mt-16 w-1/2" :src="illustrationUrl" alt="logo-RAJD" />
|
||||
<div class="mt-10 text-4xl font-medium leading-tight text-white">
|
||||
Sahkan E-mel
|
||||
</div>
|
||||
<div class="mt-5 text-lg text-white opacity-60">
|
||||
Masukkan kod 6 digit yang dihantar ke e-mel anda.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-10 flex h-screen py-5 xl:my-0 xl:h-auto xl:py-0">
|
||||
<Box raised="double"
|
||||
class="mx-auto my-auto w-full px-5 py-8 sm:w-3/4 sm:px-8 lg:w-2/4 xl:ml-24 xl:w-auto xl:p-0 xl:before:hidden xl:after:hidden xl:shadow-none xl:border-none xl:bg-none">
|
||||
<h2 class="text-center text-2xl font-semibold xl:text-left xl:text-3xl">
|
||||
Verify Email
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm opacity-70 xl:text-left">
|
||||
Kod OTP 6 digit telah dihantar ke e-mel anda.
|
||||
</p>
|
||||
|
||||
<AlertRoot v-if="errorMessage" class="mt-6" variant="danger">
|
||||
<AlertTitle>Verification failed</AlertTitle>
|
||||
<AlertDescription>{{ errorMessage }}</AlertDescription>
|
||||
</AlertRoot>
|
||||
|
||||
<AlertRoot v-if="resendMessage" class="mt-6" variant="primary">
|
||||
<AlertDescription>{{ resendMessage }}</AlertDescription>
|
||||
</AlertRoot>
|
||||
|
||||
<form class="mt-8 flex flex-col gap-5" @submit.prevent="handleVerify">
|
||||
<Input v-model="email" class="box block min-w-full px-5 py-6 xl:min-w-md" type="email"
|
||||
placeholder="Email" autocomplete="email" required />
|
||||
<Input :model-value="otp"
|
||||
class="box block min-w-full px-5 py-6 xl:min-w-md text-center tracking-[0.5em] text-lg" type="text"
|
||||
inputmode="numeric" pattern="[0-9]*" maxlength="6" placeholder="000000" autocomplete="one-time-code"
|
||||
required @input="onOtpInput" />
|
||||
|
||||
<div class="mt-5 text-center xl:mt-10 xl:text-left">
|
||||
<Button class="box w-full px-4 py-5" variant="primary" type="submit"
|
||||
:disabled="loading || !canSubmit">
|
||||
{{ loading ? 'Verifying...' : 'Verify Email' }}
|
||||
</Button>
|
||||
<Button class="box mt-4 w-full px-4 py-5" look="outline" type="button"
|
||||
:disabled="resendLoading || !email" @click="handleResend">
|
||||
{{ resendLoading ? 'Sending...' : 'Resend Code' }}
|
||||
</Button>
|
||||
<Button class="box mt-4 w-full px-4 py-5" look="outline" type="button"
|
||||
@click="router.push({ name: 'login' })">
|
||||
Back to Login
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const authPublicRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('./pages/Login.vue'),
|
||||
meta: { module: 'auth' },
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: () => import('./pages/Register.vue'),
|
||||
meta: { module: 'auth' },
|
||||
},
|
||||
{
|
||||
path: '/verify-email',
|
||||
name: 'verify-email',
|
||||
component: () => import('./pages/VerifyEmail.vue'),
|
||||
meta: { module: 'auth' },
|
||||
},
|
||||
{
|
||||
path: '/account-pending',
|
||||
name: 'account-pending',
|
||||
component: () => import('./pages/AccountPending.vue'),
|
||||
meta: { module: 'auth' },
|
||||
},
|
||||
{
|
||||
path: '/privacy-policy',
|
||||
name: 'privacy-policy',
|
||||
component: () => import('./pages/PrivacyPolicy.vue'),
|
||||
meta: { module: 'auth' },
|
||||
},
|
||||
{
|
||||
path: '/terms',
|
||||
name: 'terms',
|
||||
component: () => import('./pages/TermsConditions.vue'),
|
||||
meta: { module: 'auth' },
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,90 @@
|
||||
import { api } from '@/core/services/api'
|
||||
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
|
||||
import type {
|
||||
LoginCredentials,
|
||||
LoginResponse,
|
||||
LoginVerificationRequiredData,
|
||||
RegisterCredentials,
|
||||
RegisterResponse,
|
||||
ResendVerificationResponse,
|
||||
SessionResponse,
|
||||
SwitchRoleResponse,
|
||||
VerifyEmailPayload,
|
||||
VerifyEmailResponse,
|
||||
} from '../types/auth.types'
|
||||
|
||||
export async function login(credentials: LoginCredentials): Promise<LoginResponse> {
|
||||
const { data } = await api.post<LoginResponse>('/login', credentials)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function register(credentials: RegisterCredentials): Promise<RegisterResponse> {
|
||||
const { data } = await api.post<RegisterResponse>('/register', credentials)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function verifyEmail(payload: VerifyEmailPayload): Promise<VerifyEmailResponse> {
|
||||
const { data } = await api.post<VerifyEmailResponse>('/verify-email', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
export async function resendVerificationEmail(email: string): Promise<ResendVerificationResponse> {
|
||||
const { data } = await api.post<ResendVerificationResponse>('/verify-email/resend', { email })
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchCurrentUser(): Promise<SessionResponse> {
|
||||
const { data } = await api.get<SessionResponse>('/v1/me')
|
||||
return data
|
||||
}
|
||||
|
||||
export async function switchActiveRole(roleId: string): Promise<SwitchRoleResponse> {
|
||||
const { data } = await api.post<SwitchRoleResponse>('/v1/active-role/switch', { role_id: roleId })
|
||||
return data
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
await api.post('/v1/logout')
|
||||
}
|
||||
|
||||
export function getAuthErrorMessage(error: unknown): string {
|
||||
return getApiErrorMessage(error, 'Login failed. Please try again.')
|
||||
}
|
||||
|
||||
export function getRegisterErrorMessage(error: unknown): string {
|
||||
return getApiErrorMessage(error, 'Registration failed. Please try again.')
|
||||
}
|
||||
|
||||
export function getVerifyEmailErrorMessage(error: unknown): string {
|
||||
return getApiErrorMessage(error, 'Email verification failed. Please try again.')
|
||||
}
|
||||
|
||||
export function isAccountPending(user: { status: string } | null | undefined): boolean {
|
||||
return user?.status === 'pending'
|
||||
}
|
||||
|
||||
export function isLoginVerificationRequired(
|
||||
response: LoginResponse,
|
||||
): response is LoginResponse & { data: LoginVerificationRequiredData } {
|
||||
return 'requires_email_verification' in response.data
|
||||
&& response.data.requires_email_verification === true
|
||||
}
|
||||
|
||||
export function resolvePostAuthRoute(
|
||||
user: { status: string } | null | undefined,
|
||||
redirectPath?: string,
|
||||
) {
|
||||
if (isAccountPending(user)) {
|
||||
return { name: 'account-pending' }
|
||||
}
|
||||
|
||||
const routes: Record<string, { name: string }> = {
|
||||
'/profile': { name: 'profile-overview-2' },
|
||||
}
|
||||
|
||||
return routes[redirectPath ?? ''] ?? { name: 'profile-overview-2' }
|
||||
}
|
||||
|
||||
export function resolvePostLoginRoute(redirectPath: string, user?: { status: string } | null) {
|
||||
return resolvePostAuthRoute(user, redirectPath)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
export interface SessionResponse {
|
||||
success: boolean
|
||||
data: AuthUser
|
||||
active_role: AuthRole | null
|
||||
can_switch_role: boolean
|
||||
redirect_path: string
|
||||
}
|
||||
|
||||
export interface LoginCredentials {
|
||||
email: string
|
||||
password: string
|
||||
remember?: boolean
|
||||
}
|
||||
|
||||
export interface AuthRole {
|
||||
id: string
|
||||
name: string
|
||||
fullname: string | null
|
||||
guard_name: string
|
||||
context: string
|
||||
}
|
||||
|
||||
export interface AuthPermission {
|
||||
id: string
|
||||
name: string
|
||||
guard_name: string
|
||||
route_name: string | null
|
||||
}
|
||||
|
||||
export interface AuthUser {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
ic_number: string | null
|
||||
position: string | null
|
||||
phone_number: string | null
|
||||
image_url: string | null
|
||||
status: string
|
||||
roles?: Array<AuthRole & { permissions?: AuthPermission[] }>
|
||||
}
|
||||
|
||||
export interface LoginSessionData {
|
||||
user: AuthUser
|
||||
token_type: string
|
||||
expires_at: string
|
||||
}
|
||||
|
||||
export interface LoginVerificationRequiredData {
|
||||
email: string
|
||||
requires_email_verification: true
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
data: LoginSessionData | LoginVerificationRequiredData
|
||||
active_role?: AuthRole | null
|
||||
can_switch_role?: boolean
|
||||
redirect_path?: string
|
||||
}
|
||||
|
||||
export interface SwitchRoleResponse extends SessionResponse {
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface RegisterCredentials {
|
||||
name: string
|
||||
email: string
|
||||
ic_number: string
|
||||
password: string
|
||||
password_confirmation: string
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
data: {
|
||||
email: string
|
||||
requires_email_verification: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface VerifyEmailPayload {
|
||||
email: string
|
||||
otp: string
|
||||
}
|
||||
|
||||
export type VerifyEmailResponse = LoginResponse
|
||||
|
||||
export interface ResendVerificationResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
Reference in New Issue
Block a user