first commit

This commit is contained in:
Adrian Zürcher
2025-10-12 14:56:18 +02:00
parent a9f2e11fe6
commit a908db4f38
92 changed files with 13273 additions and 0 deletions

0
src/boot/.gitkeep Normal file
View File

23
src/boot/auth.ts Normal file
View File

@@ -0,0 +1,23 @@
import { boot } from 'quasar/wrappers';
import { appApi } from './axios';
import { createPinia } from 'pinia';
import { useUserStore } from 'src/vueLib/login/userStore';
import { useLogin } from 'src/vueLib/login/useLogin';
const pinia = createPinia();
export default boot(async ({ app }) => {
app.use(pinia);
const useStore = useUserStore();
const login = useLogin();
await appApi
.get('/login/me')
.then((resp) => {
useStore.setUser({ username: resp.data.username, role: resp.data.role });
login.refresh().catch((err) => console.error(err));
})
.catch(() => {
login.logout().catch((err) => console.error(err));
});
});

125
src/boot/axios.ts Normal file
View File

@@ -0,0 +1,125 @@
import { boot } from 'quasar/wrappers';
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import axios from 'axios';
import { useLogin } from 'src/vueLib/login/useLogin';
const host = window.location.hostname;
export const portApp = 9500;
// Create axios instance
export const appApi: AxiosInstance = axios.create({
baseURL: `http://${host}:${portApp}/api`,
timeout: 10000,
withCredentials: true,
});
interface RetryRequestConfig extends AxiosRequestConfig {
_retry?: boolean;
}
const noRefreshEndpoints = ['/login', '/secure/login/refresh', '/logout'];
// ========= Refresh Queue Handling ========= //
let isRefreshing = false;
interface FailedRequest {
resolve: (value?: unknown) => void;
reject: (reason?: Error) => void;
}
let failedQueue: FailedRequest[] = [];
const processQueue = (error: Error | null): void => {
failedQueue.forEach((prom) => {
if (error) prom.reject(error);
else prom.resolve();
});
failedQueue = [];
};
// ========================================= //
appApi.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError<unknown, RetryRequestConfig>): Promise<AxiosResponse> => {
const { refresh, logout } = useLogin();
const originalRequest = error.config as RetryRequestConfig | undefined;
// Skip refresh for login/logout endpoints
if (
!originalRequest ||
noRefreshEndpoints.some((url) => originalRequest.url?.includes(url ?? ''))
) {
const message = error instanceof Error ? error.message : JSON.stringify(error);
throw new Error(message);
}
// Handle unauthorized responses
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// Wait until refresh completes
return new Promise<AxiosResponse>((resolve, reject) => {
failedQueue.push({
resolve: () => {
void appApi(originalRequest).then(resolve).catch(reject);
},
reject,
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshed = await refresh().catch(() => false);
processQueue(null);
if (refreshed) {
// Token refreshed successfully → retry request
return appApi(originalRequest);
}
// Refresh returned false → logout
console.warn('[Axios] Refresh returned false, logging out');
await logout();
throw new Error('Token refresh failed');
} catch (err) {
const e = err instanceof Error ? err : new Error(String(err));
console.error('[Axios] Token refresh failed:', e.message);
// Always logout, even if refresh throws
try {
await logout();
} catch (logoutErr) {
console.error('[Axios] Logout failed after token refresh error:', logoutErr);
}
processQueue(e);
throw e;
} finally {
isRefreshing = false;
}
}
// Not a 401 — rethrow as Error
let msg = '';
if (error && (error as AxiosError).isAxiosError) {
const axiosError = error as AxiosError<{ message?: string }>;
msg = axiosError.response?.data?.message ?? axiosError.message;
} else {
msg = error instanceof Error ? error.message : JSON.stringify(error);
}
throw new Error(msg);
},
);
// ======== Boot registration for Quasar ======== //
export default boot(({ app }) => {
app.config.globalProperties.$axios = axios;
app.config.globalProperties.$appApi = appApi;
});
export { axios };

38
src/boot/lang.js Normal file
View File

@@ -0,0 +1,38 @@
import { createI18n } from 'vue-i18n';
import yaml from 'js-yaml';
export const lang = [];
const systemLocale = navigator.language || 'en-US';
const savedLang = localStorage.getItem('lang');
const messages = {};
const modules = import.meta.glob('src/assets/lang/*.yaml', {
eager: true,
import: 'default',
query: '?raw',
});
for (const path in modules) {
const raw = modules[path];
const parsed = yaml.load(raw);
// Extract the file name (e.g., "en.yaml" → "en")
const locale = path.split('/').pop().replace('.yaml', '');
lang.push(locale);
messages[locale] = parsed;
}
const i18n = createI18n({
legacy: false, // Composition API mode
locale: savedLang || systemLocale,
fallbackLocale: systemLocale,
messages,
});
export default ({ app }) => {
app.use(i18n);
};
export { i18n };

19
src/boot/quasar-global.ts Normal file
View File

@@ -0,0 +1,19 @@
import { boot } from 'quasar/wrappers';
import { setQuasarInstance } from 'src/utils/globalQ';
import { setRouterInstance } from 'src/utils/globalRouter';
import { databaseName } from 'src/vueLib/tables/members/MembersTable';
import { Logo } from 'src/vueLib/models/logo';
export default boot(({ app, router }) => {
setRouterInstance(router); // store router for global access
const $q = app.config.globalProperties.$q;
setQuasarInstance($q);
Logo.value = localStorage.getItem('icon') ?? '';
databaseName.value = localStorage.getItem('databaseName') ?? '';
document.documentElement.style.setProperty('--q-primary', localStorage.getItem('primaryColor'));
document.documentElement.style.setProperty(
'--q-secondary',
localStorage.getItem('secondaryColor'),
);
});