initial commit. Cloned timetracker repository

This commit is contained in:
Daniel Goc
2026-03-10 09:02:57 +01:00
commit f2952bcef0
189 changed files with 21334 additions and 0 deletions

204
bo/src/stores/auth.ts Normal file
View File

@@ -0,0 +1,204 @@
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/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,
) {
loading.value = true
error.value = null
try {
await useFetchJson('/api/v1/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ first_name, last_name, email, password, confirm_password, lang: lang || 'en' }),
})
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/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/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/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/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/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,
}
})

14
bo/src/stores/settings.ts Normal file
View File

@@ -0,0 +1,14 @@
import { useFetchJson } from '@/composable/useFetchJson'
import type { Resp } from '@/types'
import type { Settings } from '@/types/settings'
import { defineStore } from 'pinia'
export const useSettingsStore = defineStore('settings', () => {
async function getSettings() {
const { items } = await useFetchJson<Resp<Settings>>('/api/v1/settings',)
console.log(items);
}
getSettings()
return {}
})

47
bo/src/stores/theme.ts Normal file
View File

@@ -0,0 +1,47 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useThemeStore = defineStore('theme', () => {
const vueuseColorScheme = ref(localStorage.getItem('vueuse-color-scheme'))
const themeIcon = computed(() => {
switch (true) {
case vueuseColorScheme.value == 'light':
return 'i-heroicons-sun'
case vueuseColorScheme.value == 'dark':
return 'i-heroicons-moon'
case vueuseColorScheme.value == 'auto':
return 'i-heroicons-computer-desktop'
}
})
function setTheme() {
switch (true) {
case localStorage.getItem('vueuse-color-scheme') == 'dark':
vueuseColorScheme.value = 'light'
localStorage.setItem('vueuse-color-scheme', 'light')
document.documentElement.classList.toggle('dark', false)
break
case localStorage.getItem('vueuse-color-scheme') == 'light':
vueuseColorScheme.value = 'dark'
localStorage.setItem('vueuse-color-scheme', 'dark')
document.documentElement.classList.toggle('dark', true)
break
case localStorage.getItem('vueuse-color-scheme') == 'auto':
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
vueuseColorScheme.value = 'light'
localStorage.setItem('vueuse-color-scheme', 'light')
document.documentElement.classList.toggle('dark', false)
} else {
vueuseColorScheme.value = 'light'
localStorage.setItem('vueuse-color-scheme', 'light')
document.documentElement.classList.toggle('dark', false)
}
break
}
}
return {
vueuseColorScheme,
setTheme,
themeIcon,
}
})