233 lines
6.3 KiB
TypeScript
233 lines
6.3 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { useFetchJson } from '@/composable/useFetchJson'
|
|
|
|
export interface User {
|
|
id: string
|
|
email: string
|
|
name: string
|
|
}
|
|
|
|
interface AuthResponse {
|
|
access_token: string
|
|
token_type: string
|
|
expires_in: number
|
|
user: User
|
|
}
|
|
|
|
// Read the non-HTTPOnly is_authenticated cookie set by the backend.
|
|
// The backend sets it to "1" on login and removes it on logout.
|
|
function readIsAuthenticatedCookie(): boolean {
|
|
if (typeof document === 'undefined') return false
|
|
return document.cookie.split('; ').some((c) => c === 'is_authenticated=1')
|
|
}
|
|
|
|
export const useAuthStore = defineStore('auth', () => {
|
|
// useRouter must be called at the top level of the setup function, not inside a method
|
|
// We use window.location as a fallback-safe redirect mechanism instead, to avoid
|
|
// the "Cannot read properties of undefined" error when the router is not yet available.
|
|
const user = ref<User | null>(null)
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
// Auth state is derived from the is_authenticated cookie (set/cleared by backend).
|
|
// We use a ref so Vue reactivity works; it is initialised from the cookie on store creation.
|
|
const _isAuthenticated = ref<boolean>(readIsAuthenticatedCookie())
|
|
|
|
const isAuthenticated = computed(() => _isAuthenticated.value)
|
|
|
|
/** Call after any successful login to sync the reactive flag. */
|
|
function _syncAuthState() {
|
|
_isAuthenticated.value = readIsAuthenticatedCookie()
|
|
}
|
|
|
|
async function login(email: string, password: string) {
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const data = await useFetchJson<AuthResponse>('/api/v1/public/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password }),
|
|
})
|
|
|
|
const response = (data as any).items || data
|
|
|
|
if (!response.access_token) {
|
|
throw new Error('No access token received')
|
|
}
|
|
|
|
user.value = response.user
|
|
_syncAuthState()
|
|
|
|
return true
|
|
} catch (e: any) {
|
|
error.value = e?.error ?? 'An error occurred'
|
|
return false
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function register(
|
|
first_name: string,
|
|
last_name: string,
|
|
email: string,
|
|
password: string,
|
|
confirm_password: string,
|
|
lang?: string,
|
|
company_name?: string,
|
|
company_email?: string,
|
|
company_address?: {
|
|
street: string
|
|
zipCode: string
|
|
city: string
|
|
country: string
|
|
},
|
|
regon?: string,
|
|
nip?: string,
|
|
vat?: string,
|
|
billing_address?: {
|
|
street: string
|
|
zipCode: string
|
|
city: string
|
|
country: string
|
|
},
|
|
) {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const body: any = { first_name, last_name, email, password, confirm_password, lang: lang || 'en' }
|
|
|
|
// Add company information if provided
|
|
if (company_name) body.company_name = company_name
|
|
if (company_email) body.company_email = company_email
|
|
if (company_address) body.company_address = company_address
|
|
if (regon) body.regon = regon
|
|
if (nip) body.nip = nip
|
|
if (vat) body.vat = vat
|
|
if (billing_address) body.billing_address = billing_address
|
|
|
|
await useFetchJson('/api/v1/public/auth/register', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
|
|
return { success: true, requiresVerification: true }
|
|
} catch (e: any) {
|
|
error.value = e?.error ?? 'An error occurred'
|
|
return { success: false, requiresVerification: false }
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function requestPasswordReset(email: string) {
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
await useFetchJson('/api/v1/public/auth/forgot-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email }),
|
|
})
|
|
|
|
return true
|
|
} catch (e: any) {
|
|
error.value = e?.error ?? 'An error occurred'
|
|
return false
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function resetPassword(token: string, password: string) {
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
await useFetchJson('/api/v1/public/auth/reset-password', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token, password }),
|
|
})
|
|
|
|
return true
|
|
} catch (e: any) {
|
|
error.value = e?.error ?? 'An error occurred'
|
|
return false
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function loginWithGoogle() {
|
|
window.location.href = '/api/v1/public/auth/google'
|
|
}
|
|
|
|
/**
|
|
* Logout: calls the backend to revoke the refresh token and clear HTTPOnly cookies,
|
|
* clears local reactive state, then redirects to the login page.
|
|
*/
|
|
async function logout() {
|
|
try {
|
|
await useFetchJson('/api/v1/public/auth/logout', {
|
|
method: 'POST',
|
|
})
|
|
} catch {
|
|
// Continue with local cleanup even if the backend call fails
|
|
} finally {
|
|
user.value = null
|
|
_isAuthenticated.value = false
|
|
// Use dynamic import to get the router instance safely from outside the setup context
|
|
const { default: router } = await import('@/router')
|
|
router.push({ name: 'login' })
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh the access token by calling the backend.
|
|
* The backend reads the HTTPOnly refresh_token cookie, rotates it, and sets new cookies.
|
|
* Returns true on success.
|
|
*/
|
|
async function refreshAccessToken(): Promise<boolean> {
|
|
try {
|
|
await useFetchJson('/api/v1/public/auth/refresh', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
// No body needed — the backend reads the refresh_token from the HTTPOnly cookie
|
|
})
|
|
|
|
_syncAuthState()
|
|
return true
|
|
} catch {
|
|
// Refresh failed — clear local state
|
|
user.value = null
|
|
_isAuthenticated.value = false
|
|
return false
|
|
}
|
|
}
|
|
|
|
function clearError() {
|
|
error.value = null
|
|
}
|
|
|
|
return {
|
|
user,
|
|
loading,
|
|
error,
|
|
isAuthenticated,
|
|
login,
|
|
loginWithGoogle,
|
|
register,
|
|
requestPasswordReset,
|
|
resetPassword,
|
|
logout,
|
|
refreshAccessToken,
|
|
clearError,
|
|
}
|
|
})
|