diff --git a/apps/portal/tests/composables/useFormDraft.submitter.test.ts b/apps/app/src/composables/__tests__/useFormDraft.submitter.test.ts similarity index 100% rename from apps/portal/tests/composables/useFormDraft.submitter.test.ts rename to apps/app/src/composables/__tests__/useFormDraft.submitter.test.ts diff --git a/apps/portal/tests/composables/useFormDraft.test.ts b/apps/app/src/composables/__tests__/useFormDraft.test.ts similarity index 100% rename from apps/portal/tests/composables/useFormDraft.test.ts rename to apps/app/src/composables/__tests__/useFormDraft.test.ts diff --git a/apps/portal/tests/composables/api/usePublicFormSections.spec.ts b/apps/app/src/composables/api/__tests__/usePublicFormSections.spec.ts similarity index 100% rename from apps/portal/tests/composables/api/usePublicFormSections.spec.ts rename to apps/app/src/composables/api/__tests__/usePublicFormSections.spec.ts diff --git a/apps/portal/tests/composables/api/usePublicFormTimeSlots.spec.ts b/apps/app/src/composables/api/__tests__/usePublicFormTimeSlots.spec.ts similarity index 100% rename from apps/portal/tests/composables/api/usePublicFormTimeSlots.spec.ts rename to apps/app/src/composables/api/__tests__/usePublicFormTimeSlots.spec.ts diff --git a/apps/app/src/composables/api/portal/.gitkeep b/apps/app/src/composables/api/portal/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/portal/src/composables/api/usePortalProfile.ts b/apps/app/src/composables/api/portal/usePortalProfile.ts similarity index 100% rename from apps/portal/src/composables/api/usePortalProfile.ts rename to apps/app/src/composables/api/portal/usePortalProfile.ts diff --git a/apps/portal/src/composables/api/usePortalShifts.ts b/apps/app/src/composables/api/portal/usePortalShifts.ts similarity index 100% rename from apps/portal/src/composables/api/usePortalShifts.ts rename to apps/app/src/composables/api/portal/usePortalShifts.ts diff --git a/apps/portal/src/composables/api/useVolunteerRegistration.ts b/apps/app/src/composables/api/portal/useVolunteerRegistration.ts similarity index 100% rename from apps/portal/src/composables/api/useVolunteerRegistration.ts rename to apps/app/src/composables/api/portal/useVolunteerRegistration.ts diff --git a/apps/portal/src/composables/api/usePublicForm.ts b/apps/app/src/composables/api/usePublicForm.ts similarity index 100% rename from apps/portal/src/composables/api/usePublicForm.ts rename to apps/app/src/composables/api/usePublicForm.ts diff --git a/apps/portal/src/composables/api/usePublicFormSections.ts b/apps/app/src/composables/api/usePublicFormSections.ts similarity index 100% rename from apps/portal/src/composables/api/usePublicFormSections.ts rename to apps/app/src/composables/api/usePublicFormSections.ts diff --git a/apps/portal/src/composables/api/usePublicFormTimeSlots.ts b/apps/app/src/composables/api/usePublicFormTimeSlots.ts similarity index 100% rename from apps/portal/src/composables/api/usePublicFormTimeSlots.ts rename to apps/app/src/composables/api/usePublicFormTimeSlots.ts diff --git a/apps/app/src/composables/portal/.gitkeep b/apps/app/src/composables/portal/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/portal/src/composables/publicFormInjection.ts b/apps/app/src/composables/publicFormInjection.ts similarity index 100% rename from apps/portal/src/composables/publicFormInjection.ts rename to apps/app/src/composables/publicFormInjection.ts diff --git a/apps/portal/src/composables/useFormDraft.ts b/apps/app/src/composables/useFormDraft.ts similarity index 100% rename from apps/portal/src/composables/useFormDraft.ts rename to apps/app/src/composables/useFormDraft.ts diff --git a/apps/portal/src/schemas/registrationSchema.ts b/apps/app/src/schemas/registrationSchema.ts similarity index 100% rename from apps/portal/src/schemas/registrationSchema.ts rename to apps/app/src/schemas/registrationSchema.ts diff --git a/apps/portal/src/types/api.ts b/apps/app/src/types/api.ts similarity index 100% rename from apps/portal/src/types/api.ts rename to apps/app/src/types/api.ts diff --git a/apps/portal/src/types/portal-shift.ts b/apps/app/src/types/portal-shift.ts similarity index 100% rename from apps/portal/src/types/portal-shift.ts rename to apps/app/src/types/portal-shift.ts diff --git a/apps/portal/src/types/portal.ts b/apps/app/src/types/portal.ts similarity index 100% rename from apps/portal/src/types/portal.ts rename to apps/app/src/types/portal.ts diff --git a/apps/portal/src/types/registration.ts b/apps/app/src/types/registration.ts similarity index 100% rename from apps/portal/src/types/registration.ts rename to apps/app/src/types/registration.ts diff --git a/apps/portal/src/composables/api/useMfa.ts b/apps/portal/src/composables/api/useMfa.ts deleted file mode 100644 index cb8c0dc8..00000000 --- a/apps/portal/src/composables/api/useMfa.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' -import { apiClient } from '@/lib/axios' -import type { - MfaConfirmResponse, - MfaStatus, - MfaTotpSetup, - MfaVerifyPayload, - TrustedDevice, -} from '@/types/mfa' - -interface ApiResponse { - success: boolean - data: T - message: string -} - -// ─── Setup ─── - -export function useSetupTotp() { - return useMutation({ - mutationFn: async () => { - const { data } = await apiClient.post>('/auth/mfa/setup/totp') - - return data.data - }, - }) -} - -export function useConfirmTotp() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (code: string) => { - const { data } = await apiClient.post>('/auth/mfa/setup/totp/confirm', { code }) - - return data.data - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mfa-status'] }) - }, - }) -} - -export function useSetupEmail() { - return useMutation({ - mutationFn: async () => { - const { data } = await apiClient.post>('/auth/mfa/setup/email') - - return data - }, - }) -} - -export function useConfirmEmail() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (code: string) => { - const { data } = await apiClient.post>('/auth/mfa/setup/email/confirm', { code }) - - return data.data - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mfa-status'] }) - }, - }) -} - -// ─── Management ─── - -export function useDisableMfa() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (payload: { code: string; method: string }) => { - const { data } = await apiClient.post>('/auth/mfa/disable', payload) - - return data - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mfa-status'] }) - }, - }) -} - -export function useRegenerateBackupCodes() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (payload: { code: string }) => { - const { data } = await apiClient.post>('/auth/mfa/backup-codes', payload) - - return data.data - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mfa-status'] }) - }, - }) -} - -export function useMfaStatus() { - return useQuery({ - queryKey: ['mfa-status'], - queryFn: async () => { - const { data } = await apiClient.get>('/auth/mfa/status') - - return data.data - }, - }) -} - -export function useSetPreferredMethod() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (method: 'totp' | 'email') => { - const { data } = await apiClient.put>('/auth/mfa/preferred-method', { method }) - - return data.data - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mfa-status'] }) - }, - }) -} - -// ─── Trusted devices ─── - -export function useTrustedDevices() { - return useQuery({ - queryKey: ['trusted-devices'], - queryFn: async () => { - const { data } = await apiClient.get>('/auth/trusted-devices') - - return data.data - }, - }) -} - -export function useRevokeDevice() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (id: string) => { - await apiClient.delete(`/auth/trusted-devices/${id}`) - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['trusted-devices'] }) - }, - }) -} - -export function useRevokeAllDevices() { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async () => { - await apiClient.delete('/auth/trusted-devices') - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['trusted-devices'] }) - }, - }) -} - -// ─── Login flow (no auth needed — uses session token) ─── - -export function useVerifyMfa() { - return useMutation({ - mutationFn: async (payload: MfaVerifyPayload) => { - const { data } = await apiClient.post('/auth/mfa/verify', payload) - - return data - }, - }) -} - -export function useSendMfaEmailCode() { - return useMutation({ - mutationFn: async (mfaSessionToken: string) => { - const { data } = await apiClient.post('/auth/mfa/email/send', { - mfa_session_token: mfaSessionToken, - }) - - return data - }, - }) -} diff --git a/apps/portal/src/lib/axios.ts b/apps/portal/src/lib/axios.ts deleted file mode 100644 index 0b5a78c6..00000000 --- a/apps/portal/src/lib/axios.ts +++ /dev/null @@ -1,55 +0,0 @@ -import axios from 'axios' -import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios' - -const apiClient: AxiosInstance = axios.create({ - baseURL: import.meta.env.VITE_API_URL, - withCredentials: true, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - timeout: 30000, -}) - -apiClient.interceptors.request.use( - (config: InternalAxiosRequestConfig) => { - if (import.meta.env.DEV) { - console.log(`🚀 ${config.method?.toUpperCase()} ${config.url}`, config.data) - } - - return config - }, - error => Promise.reject(error), -) - -apiClient.interceptors.response.use( - response => { - if (import.meta.env.DEV) { - console.log(`✅ ${response.status} ${response.config.url}`, response.data) - } - - return response - }, - error => { - if (import.meta.env.DEV) { - console.error( - `❌ ${error.response?.status} ${error.config?.url}`, - error.response?.data, - ) - } - - if (error.response?.status === 401) { - // Lazy import to avoid circular dependency - import('@/stores/useAuthStore').then(({ useAuthStore }) => { - const authStore = useAuthStore() - if (authStore.isInitialized) { - authStore.handleUnauthorized() - } - }) - } - - return Promise.reject(error) - }, -) - -export { apiClient } diff --git a/apps/portal/src/lib/query-client.ts b/apps/portal/src/lib/query-client.ts deleted file mode 100644 index 9f90d290..00000000 --- a/apps/portal/src/lib/query-client.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { VueQueryPluginOptions } from '@tanstack/vue-query' - -export const queryClientConfig: VueQueryPluginOptions = { - queryClientConfig: { - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, // 5 minutes - retry: 1, - }, - }, - }, -} diff --git a/apps/portal/src/types/mfa.ts b/apps/portal/src/types/mfa.ts deleted file mode 100644 index 3bbb2555..00000000 --- a/apps/portal/src/types/mfa.ts +++ /dev/null @@ -1,63 +0,0 @@ -export const MfaMethod = { - TOTP: 'totp', - EMAIL: 'email', - BACKUP_CODE: 'backup_code', -} as const - -export type MfaMethod = typeof MfaMethod[keyof typeof MfaMethod] - -export interface MfaStatus { - enabled: boolean - method: 'totp' | 'email' | null - confirmed_at: string | null - backup_codes_remaining: number - is_required: boolean - totp_configured: boolean - email_configured: boolean -} - -export interface MfaUserStatus { - enabled: boolean - method: 'totp' | 'email' | null - confirmed_at: string | null - setup_required: boolean -} - -export interface MfaTotpSetup { - secret: string - qr_code_url: string - provisioning_uri: string -} - -export interface MfaSessionResponse { - success: true - mfa_required: true - mfa_session_token: string - methods: MfaMethod[] - preferred_method: 'totp' | 'email' - expires_in: number -} - -export interface MfaConfirmResponse { - mfa_enabled: boolean - method: 'totp' | 'email' - backup_codes: string[] -} - -export interface TrustedDevice { - id: string - device_name: string | null - ip_address: string - trusted_until: string - last_used_at: string | null - created_at: string -} - -export interface MfaVerifyPayload { - mfa_session_token: string - code: string - method: MfaMethod - trust_device?: boolean - device_fingerprint?: string - device_name?: string -} diff --git a/apps/portal/src/utils/deviceFingerprint.ts b/apps/portal/src/utils/deviceFingerprint.ts deleted file mode 100644 index 89f86154..00000000 --- a/apps/portal/src/utils/deviceFingerprint.ts +++ /dev/null @@ -1,49 +0,0 @@ -export function generateDeviceFingerprint(): string { - const components = [ - navigator.userAgent, - `${screen.width}x${screen.height}`, - Intl.DateTimeFormat().resolvedOptions().timeZone, - navigator.language, - navigator.hardwareConcurrency?.toString() ?? '', - ] - - const str = components.join('|') - let hash = 0 - - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i) - - hash = ((hash << 5) - hash) + char - hash = hash & hash - } - - return Math.abs(hash).toString(36) -} - -export function getDeviceName(): string { - const ua = navigator.userAgent - let browser = 'Onbekend' - let os = 'Onbekend' - - if (ua.includes('Chrome') && !ua.includes('Edg')) - browser = 'Chrome' - else if (ua.includes('Firefox')) - browser = 'Firefox' - else if (ua.includes('Safari') && !ua.includes('Chrome')) - browser = 'Safari' - else if (ua.includes('Edg')) - browser = 'Edge' - - if (ua.includes('Mac OS')) - os = 'macOS' - else if (ua.includes('Windows')) - os = 'Windows' - else if (ua.includes('Linux')) - os = 'Linux' - else if (ua.includes('Android')) - os = 'Android' - else if (ua.includes('iPhone') || ua.includes('iPad')) - os = 'iOS' - - return `${browser} op ${os}` -} diff --git a/apps/portal/src/utils/paginationMeta.ts b/apps/portal/src/utils/paginationMeta.ts deleted file mode 100644 index c94fe329..00000000 --- a/apps/portal/src/utils/paginationMeta.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const paginationMeta = (options: T, total: number) => { - const start = (options.page - 1) * options.itemsPerPage + 1 - const end = Math.min(options.page * options.itemsPerPage, total) - - return `Showing ${total === 0 ? 0 : start} to ${end} of ${total} entries` -}