first commit
This commit is contained in:
0
src/boot/.gitkeep
Normal file
0
src/boot/.gitkeep
Normal file
23
src/boot/auth.ts
Normal file
23
src/boot/auth.ts
Normal 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
125
src/boot/axios.ts
Normal 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
38
src/boot/lang.js
Normal 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
19
src/boot/quasar-global.ts
Normal 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'),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user