From cb695c30b2b732e905fbfa370f5e422d7f326247 Mon Sep 17 00:00:00 2001 From: DmPrkp Date: Sun, 9 Nov 2025 11:54:28 +0300 Subject: [PATCH 1/2] Align client auth with external user server --- ionic-client/src/components/nav/FooterBar.vue | 76 +++-- ionic-client/src/main.ts | 70 ++++ ionic-client/src/models/AuthModel.ts | 128 +++++++ ionic-client/src/models/BaseModel.ts | 42 ++- ionic-client/src/pages/AuthPage.vue | 184 ++++++++++ ionic-client/src/pages/SettingsPage.vue | 80 ++++- ionic-client/src/plugins/i18n/locales/en.json | 22 +- ionic-client/src/plugins/i18n/locales/ru.json | 22 +- ionic-client/src/router/index.ts | 14 + ionic-client/src/store/auth.ts | 318 ++++++++++++++++++ ionic-client/src/store/index.ts | 3 +- ionic-client/src/types/dto/auth.ts | 18 + ionic-client/src/types/dto/index.ts | 3 + ionic-client/src/vite-env.d.ts | 9 + 14 files changed, 953 insertions(+), 36 deletions(-) create mode 100644 ionic-client/src/models/AuthModel.ts create mode 100644 ionic-client/src/pages/AuthPage.vue create mode 100644 ionic-client/src/store/auth.ts create mode 100644 ionic-client/src/types/dto/auth.ts diff --git a/ionic-client/src/components/nav/FooterBar.vue b/ionic-client/src/components/nav/FooterBar.vue index d207688..b5b60cf 100644 --- a/ionic-client/src/components/nav/FooterBar.vue +++ b/ionic-client/src/components/nav/FooterBar.vue @@ -38,32 +38,64 @@ settingsOutline, informationCircleOutline, documentsOutline, + logInOutline, } from "ionicons/icons"; + import { computed } from "vue"; import { useRoute } from "vue-router"; import injectI18nToRoute from "@/mixins/injectI18nToRoute"; + import { useAuthStore } from "@/store/auth"; - const menuItems = [ - { - link: "about", - name: "about", - icon: informationCircleOutline, - }, - { - link: "main", - name: "main", - icon: calculatorOutline, - }, - { - link: "zayavka", - name: "zayavka", - icon: documentsOutline, - }, - { - link: "settings", - name: "settings", - icon: settingsOutline, - }, - ]; + const authStore = useAuthStore(); + + const menuItems = computed(() => { + if (authStore.isAuthenticated) { + return [ + { + link: "about", + name: "about", + icon: informationCircleOutline, + }, + { + link: "main", + name: "main", + icon: calculatorOutline, + }, + { + link: "zayavka", + name: "zayavka", + icon: documentsOutline, + }, + { + link: "settings", + name: "settings", + icon: settingsOutline, + }, + ]; + } + + return [ + { + link: "about", + name: "about", + icon: informationCircleOutline, + }, + { + link: "auth", + name: "auth", + icon: logInOutline, + }, + { + link: "main", + name: "main", + icon: calculatorOutline, + }, + { + link: "settings", + name: "settings", + icon: settingsOutline, + }, + ]; + }); const getLocalizedRoute = (routeName: string) => { const route = useRoute(); diff --git a/ionic-client/src/main.ts b/ionic-client/src/main.ts index 8f3d265..2da2efa 100644 --- a/ionic-client/src/main.ts +++ b/ionic-client/src/main.ts @@ -17,6 +17,14 @@ import { IonPage, IonButton, IonItemDivider, + IonCard, + IonCardHeader, + IonCardTitle, + IonCardContent, + IonCardSubtitle, + IonSegment, + IonSegmentButton, + IonSpinner, } from "@ionic/vue"; import App from "./App.vue"; import router from "./router"; @@ -44,10 +52,64 @@ import "@ionic/vue/css/display.css"; import "./theme/variables.css"; import BaseModel from "./models/BaseModel"; +import AuthModel from "./models/AuthModel"; +import { useAuthStore } from "./store/auth"; const head = createHead(); const pinia = createPinia(); BaseModel.setBaseUrl(); +const defaultBaseUrl = BaseModel.baseURL; +AuthModel.setBaseUrl( + import.meta.env.VITE_USER_API_ORIGIN || + import.meta.env.VITE_AUTH_API_ORIGIN || + defaultBaseUrl +); +const authStore = useAuthStore(pinia); +authStore.initialize(); +if (authStore.isAuthenticated) { + authStore.fetchProfile().catch(() => undefined); +} + +router.beforeEach(async (to) => { + const requiresAuth = to.meta?.requiresAuth !== false; + const localeParam = + typeof to.params.locale === "string" + ? to.params.locale + : import.meta.env.VITE_DEFAULT_LOCALE; + + if (!requiresAuth) { + if (to.name === "auth" && authStore.isAuthenticated) { + const redirectTarget = + typeof to.query.redirect === "string" + ? (to.query.redirect as string) + : `/${localeParam}/main`; + return redirectTarget; + } + return true; + } + + if (!authStore.isAuthenticated) { + return { + name: "auth", + params: { locale: localeParam }, + query: { redirect: to.fullPath }, + }; + } + + if (!authStore.user) { + try { + await authStore.fetchProfile(); + } catch (error) { + return { + name: "auth", + params: { locale: localeParam }, + query: { redirect: to.fullPath }, + }; + } + } + + return true; +}); const app = createApp(App) .use(IonicVue) @@ -69,6 +131,14 @@ const app = createApp(App) .component("IonText", IonText) .component("IonPage", IonPage) .component("IonButton", IonButton) + .component("IonCard", IonCard) + .component("IonCardHeader", IonCardHeader) + .component("IonCardTitle", IonCardTitle) + .component("IonCardContent", IonCardContent) + .component("IonCardSubtitle", IonCardSubtitle) + .component("IonSegment", IonSegment) + .component("IonSegmentButton", IonSegmentButton) + .component("IonSpinner", IonSpinner) .component("MaterialList", MaterialList); router.isReady().then(() => { diff --git a/ionic-client/src/models/AuthModel.ts b/ionic-client/src/models/AuthModel.ts new file mode 100644 index 0000000..38c7bc0 --- /dev/null +++ b/ionic-client/src/models/AuthModel.ts @@ -0,0 +1,128 @@ +import BaseModel from './BaseModel'; +import type { AuthResponse } from '@/types/dto/auth'; + +const DEFAULT_AUTH_PREFIX = '/user/api/v1'; + +const parsePaths = (value: string | undefined, defaults: string[]) => { + if (!value) { + return defaults; + } + + const normalized = value.trim().toLowerCase(); + if (["none", "false", "off", "disable", "disabled"].includes(normalized)) { + return []; + } + + return value + .split(',') + .map((segment) => segment.trim()) + .filter(Boolean); +}; + +const userApiPrefix = + import.meta.env.VITE_USER_API_PREFIX || + import.meta.env.VITE_AUTH_API_PREFIX || + DEFAULT_AUTH_PREFIX; + +const loginPaths = parsePaths(import.meta.env.VITE_USER_API_LOGIN_PATHS, [ + '/auth/login', + '/login', + '/users/login', +]); + +const registerPaths = parsePaths(import.meta.env.VITE_USER_API_REGISTER_PATHS, [ + '/auth/register', + '/register', + '/users/register', + '/signup', +]); + +const profilePaths = parsePaths(import.meta.env.VITE_USER_API_PROFILE_PATHS, [ + '/profile', + '/auth/profile', + '/users/me', +]); + +const credentialsEnv = import.meta.env.VITE_USER_API_CREDENTIALS; +const allowedCredentials: RequestCredentials[] = ['omit', 'same-origin', 'include']; +const requestCredentials = allowedCredentials.includes( + credentialsEnv as RequestCredentials +) + ? (credentialsEnv as RequestCredentials) + : undefined; + +const authRequestOpts = requestCredentials + ? ({ credentials: requestCredentials } as RequestInit) + : undefined; + +export default class AuthModel extends BaseModel { + static apiVersion = userApiPrefix; + + private static async postWithFallback( + paths: string[], + body: Record + ): Promise { + let lastError: unknown; + + for (const path of paths) { + try { + return await this.post({ + params: path, + body, + opts: authRequestOpts, + }); + } catch (error) { + lastError = error; + } + } + + if (lastError instanceof Error) { + throw lastError; + } + + throw new Error('Не удалось обратиться к серверу авторизации'); + } + + private static async getWithFallback(paths: string[]) { + for (const path of paths) { + const response = await this.get(path); + if (response !== undefined) { + return response; + } + } + + return undefined; + } + + static login(email: string, password: string) { + const payload = { + email, + login: email, + username: email, + password, + password_confirmation: password, + }; + + return this.postWithFallback(loginPaths, payload); + } + + static register(email: string, password: string) { + const payload = { + email, + login: email, + username: email, + password, + password_confirmation: password, + }; + + return this.postWithFallback(registerPaths, payload); + } + + static profile() { + if (!profilePaths.length) { + return Promise.resolve(undefined); + } + + return this.getWithFallback(profilePaths); + } +} diff --git a/ionic-client/src/models/BaseModel.ts b/ionic-client/src/models/BaseModel.ts index 9aa342c..64d9501 100644 --- a/ionic-client/src/models/BaseModel.ts +++ b/ionic-client/src/models/BaseModel.ts @@ -9,6 +9,21 @@ export default class BaseModel { }, }; + static setAuthToken(token?: string, scheme = "Bearer") { + const headers = (this.baseOpts.headers || {}) as Record; + + if (token) { + const trimmedScheme = scheme?.trim(); + headers["Authorization"] = trimmedScheme + ? `${trimmedScheme} ${token}`.trim() + : token; + } else { + delete headers["Authorization"]; + } + + this.baseOpts.headers = headers; + } + static setBaseUrl(url?: string | undefined) { const port = import.meta.env.VITE_PORT ? `:${import.meta.env.VITE_PORT}` @@ -17,9 +32,28 @@ export default class BaseModel { url || `${import.meta.env.VITE_PROTOCOL}://${location.hostname + port}`; } + private static buildUrl(params: string, queries: string[] = []) { + const isAbsolute = /^https?:\/\//i.test(params); + const hasQuery = params.includes("?"); + const querySuffix = queries.length + ? `${hasQuery ? "&" : "?"}${queries.join("&")}` + : ""; + + if (isAbsolute) { + return `${params}${querySuffix}`; + } + + const base = `${this.baseURL}${this.apiVersion}${params}`; + if (!queries.length) { + return base; + } + + return `${base}${hasQuery ? "&" : "?"}${queries.join("&")}`; + } + static async get(params: string): Promise { try { - const url = this.baseURL + this.apiVersion + params; + const url = this.buildUrl(params); const response = await fetch(url); if (!response.ok) { throw new Error(response.statusText); @@ -41,8 +75,7 @@ export default class BaseModel { queries?: string[]; opts?: RequestInit; }): Promise { - const queryString = queries.length ? `?${queries.join("&")}` : ""; - const query = `${this.baseURL}${this.apiVersion}${params}${queryString}`; + const query = this.buildUrl(params, queries); const options = Object.assign( { @@ -73,8 +106,7 @@ export default class BaseModel { queries?: string[]; opts?: RequestInit; }): Promise { - const queryString = queries.length ? `?${queries.join("&")}` : ""; - const query = `${this.baseURL}${this.apiVersion}${params}${queryString}`; + const query = this.buildUrl(params, queries); const options = Object.assign( { diff --git a/ionic-client/src/pages/AuthPage.vue b/ionic-client/src/pages/AuthPage.vue new file mode 100644 index 0000000..bf3d268 --- /dev/null +++ b/ionic-client/src/pages/AuthPage.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/ionic-client/src/pages/SettingsPage.vue b/ionic-client/src/pages/SettingsPage.vue index c36dafd..5c33956 100644 --- a/ionic-client/src/pages/SettingsPage.vue +++ b/ionic-client/src/pages/SettingsPage.vue @@ -1,16 +1,84 @@ diff --git a/ionic-client/src/plugins/i18n/locales/en.json b/ionic-client/src/plugins/i18n/locales/en.json index 06944dd..65922fe 100644 --- a/ionic-client/src/plugins/i18n/locales/en.json +++ b/ionic-client/src/plugins/i18n/locales/en.json @@ -4,7 +4,8 @@ "settings": "Settings", "main": "Main", "about": "About", - "zayavka": "Material Requests" + "zayavka": "Material Requests", + "auth": "Sign in" }, "ui": { "buttons": { @@ -97,6 +98,25 @@ "title": "Materials requests", "item_title": "Materials request #", "from": "from" + }, + "auth": { + "title_login": "Sign in to your workspace", + "title_register": "Create a new account", + "subtitle": "Manage material requests after authorisation", + "login": "Sign in", + "register": "Sign up", + "email": "Email", + "password": "Password", + "confirm": "Confirm password", + "login_action": "Sign in", + "register_action": "Create account", + "password_mismatch": "Passwords do not match" + }, + "settings": { + "title": "Profile", + "email": "Email", + "joined": "Joined", + "logout": "Sign out" } }, "current": { diff --git a/ionic-client/src/plugins/i18n/locales/ru.json b/ionic-client/src/plugins/i18n/locales/ru.json index 705bcf5..1581a82 100644 --- a/ionic-client/src/plugins/i18n/locales/ru.json +++ b/ionic-client/src/plugins/i18n/locales/ru.json @@ -4,7 +4,8 @@ "settings": "Настройки", "main": "Калькулятор", "about": "О проекте", - "zayavka": "Заявки" + "zayavka": "Заявки", + "auth": "Войти" }, "ui": { "buttons": { @@ -97,6 +98,25 @@ "title": "Заявки на материалы", "item_title": "Заявка №", "from": "от" + }, + "auth": { + "title_login": "Вход в личный кабинет", + "title_register": "Регистрация", + "subtitle": "Используйте один аккаунт для работы с заявками", + "login": "Вход", + "register": "Регистрация", + "email": "Email", + "password": "Пароль", + "confirm": "Повторите пароль", + "login_action": "Войти", + "register_action": "Создать аккаунт", + "password_mismatch": "Пароли не совпадают" + }, + "settings": { + "title": "Профиль", + "email": "Электронная почта", + "joined": "Дата регистрации", + "logout": "Выйти" } }, "current": { "ac": "сетевой", "dc": "аккум" } diff --git a/ionic-client/src/router/index.ts b/ionic-client/src/router/index.ts index 7ce3975..a61ad71 100644 --- a/ionic-client/src/router/index.ts +++ b/ionic-client/src/router/index.ts @@ -20,26 +20,37 @@ const routes: Array = [ path: "about", name: "about", component: () => import("@/pages/AboutPage.vue"), + meta: { requiresAuth: true }, + }, + { + path: "auth", + name: "auth", + component: () => import("@/pages/AuthPage.vue"), + meta: { requiresAuth: false }, }, { path: "main", name: "main", component: () => import("@/pages/MainPage.vue"), + meta: { requiresAuth: true }, children: [ { path: ":workType", name: "work-type", component: () => import("@/pages/SystemsPage.vue"), + meta: { requiresAuth: true }, children: [ { path: ":system", name: "system", component: () => import("@/pages/ComponentsPage.vue"), + meta: { requiresAuth: true }, children: [ { path: "materialList", name: "material-list", component: () => import("@/pages/MaterialListPage.vue"), + meta: { requiresAuth: true }, }, ], }, @@ -51,11 +62,13 @@ const routes: Array = [ path: "zayavka", name: "zayavka-list", component: () => import("@/pages/ZayavkaListPage.vue"), + meta: { requiresAuth: true }, children: [ { path: ":zayavka", name: "zayavka", component: () => import("@/pages/ZayavkaPage.vue"), + meta: { requiresAuth: true }, }, ], }, @@ -63,6 +76,7 @@ const routes: Array = [ path: "settings", name: "settings", component: () => import("@/pages/SettingsPage.vue"), + meta: { requiresAuth: true }, }, ], }, diff --git a/ionic-client/src/store/auth.ts b/ionic-client/src/store/auth.ts new file mode 100644 index 0000000..d6e1f0b --- /dev/null +++ b/ionic-client/src/store/auth.ts @@ -0,0 +1,318 @@ +import { defineStore } from 'pinia'; +import AuthModel from '@/models/AuthModel'; +import BaseModel from '@/models/BaseModel'; +import type { AuthResponse, UserProfile } from '@/types/dto'; + +const TOKEN_STORAGE_KEY = 'mr-auth-token'; + +const normalizeScheme = (value?: string) => { + if (!value) { + return 'Bearer'; + } + + const trimmed = value.trim(); + if (["none", "off", "false", "disabled", "no"].includes(trimmed.toLowerCase())) { + return ''; + } + + return trimmed; +}; + +const AUTH_HEADER_SCHEME = normalizeScheme(import.meta.env.VITE_USER_API_AUTH_SCHEME); + +const TOKEN_KEYS = [ + 'accessToken', + 'access_token', + 'token', + 'jwt', + 'id_token', + 'sessionToken', + 'session_token', + 'bearer', +]; + +const USER_IDENTIFIER_KEYS = ['id', 'userId', 'user_id', 'uuid', 'uid']; +const USER_EMAIL_KEYS = ['email', 'mail']; +const USER_USERNAME_KEYS = ['username', 'login', 'name']; +const USER_CREATED_KEYS = ['createdAt', 'created_at', 'created_on', 'created']; +const USER_UPDATED_KEYS = ['updatedAt', 'updated_at', 'updated_on', 'updated']; + +const isPlainObject = (value: unknown): value is Record => { + return typeof value === 'object' && value !== null && !Array.isArray(value); +}; + +const findStringByKeys = (source: unknown, keys: string[]): string | undefined => { + if (Array.isArray(source)) { + for (const item of source) { + const value = findStringByKeys(item, keys); + if (value) { + return value; + } + } + return undefined; + } + + if (!isPlainObject(source)) { + return undefined; + } + + for (const key of keys) { + const candidate = source[key]; + if (typeof candidate === 'string' && candidate.trim()) { + return candidate.trim(); + } + } + + for (const value of Object.values(source)) { + const nested = findStringByKeys(value, keys); + if (nested) { + return nested; + } + } + + return undefined; +}; + +const normalizeDate = (value: unknown): string | undefined => { + if (value instanceof Date) { + return value.toISOString(); + } + + if (typeof value === 'number' && Number.isFinite(value)) { + const date = new Date(value); + if (!Number.isNaN(date.getTime())) { + return date.toISOString(); + } + } + + if (typeof value === 'string' && value.trim()) { + return value.trim(); + } + + return undefined; +}; + +const findUserCandidate = ( + source: unknown, + visited = new WeakSet() +): Record | undefined => { + if (Array.isArray(source)) { + for (const item of source) { + const candidate = findUserCandidate(item, visited); + if (candidate) { + return candidate; + } + } + return undefined; + } + + if (!isPlainObject(source)) { + return undefined; + } + + if (visited.has(source)) { + return undefined; + } + + visited.add(source); + + const hasIdentifier = USER_IDENTIFIER_KEYS.some((key) => key in source); + const hasContact = + USER_EMAIL_KEYS.some((key) => key in source) || + USER_USERNAME_KEYS.some((key) => key in source); + + if (hasIdentifier || hasContact) { + return source; + } + + for (const value of Object.values(source)) { + const nested = findUserCandidate(value, visited); + if (nested) { + return nested; + } + } + + return undefined; +}; + +const normalizeUserProfile = (payload: unknown): UserProfile | null => { + if (!payload) { + return null; + } + + const candidate = findUserCandidate(payload); + if (!candidate) { + return null; + } + + const identifier = + candidate.id ?? candidate.userId ?? candidate.user_id ?? candidate.uuid ?? candidate.uid; + + if (typeof identifier !== 'string' && typeof identifier !== 'number') { + return null; + } + + const email = findStringByKeys(candidate, USER_EMAIL_KEYS); + const username = findStringByKeys(candidate, USER_USERNAME_KEYS); + const createdAt = normalizeDate( + USER_CREATED_KEYS.map((key) => candidate[key]).find((value) => value !== undefined) + ); + const updatedAt = normalizeDate( + USER_UPDATED_KEYS.map((key) => candidate[key]).find((value) => value !== undefined) + ); + + return { + id: identifier, + email, + username, + createdAt, + updatedAt, + }; +}; + +const extractToken = (payload: unknown): string | undefined => { + if (!payload) { + return undefined; + } + + return findStringByKeys(payload, TOKEN_KEYS); +}; + +class MissingTokenError extends Error { + constructor() { + super('Пустой ответ от сервера'); + this.name = 'MissingTokenError'; + } +} + +interface AuthState { + token: string | null; + user: UserProfile | null; + status: 'idle' | 'loading' | 'error'; + error: string | null; +} + +export const useAuthStore = defineStore('auth', { + state: (): AuthState => ({ + token: null, + user: null, + status: 'idle', + error: null, + }), + getters: { + isAuthenticated: (state) => Boolean(state.token), + }, + actions: { + setToken(token: string | null) { + this.token = token; + if (token) { + localStorage.setItem(TOKEN_STORAGE_KEY, token); + } else { + localStorage.removeItem(TOKEN_STORAGE_KEY); + } + BaseModel.setAuthToken(token ?? undefined, AUTH_HEADER_SCHEME); + }, + setUser(user: UserProfile | null) { + this.user = user; + }, + clearError() { + this.error = null; + }, + async login(email: string, password: string) { + this.status = 'loading'; + this.clearError(); + try { + const response = await AuthModel.login(email, password); + this.applyAuthResponse(response); + await this.fetchProfile(); + } catch (error) { + this.status = 'error'; + this.error = + error instanceof Error ? error.message : 'Не удалось войти. Попробуйте снова.'; + throw error; + } finally { + if (this.status !== 'error') { + this.status = 'idle'; + } + } + }, + async register(email: string, password: string) { + this.status = 'loading'; + this.clearError(); + try { + const response = await AuthModel.register(email, password); + try { + this.applyAuthResponse(response); + } catch (error) { + if (error instanceof MissingTokenError) { + const loginResponse = await AuthModel.login(email, password); + this.applyAuthResponse(loginResponse); + } else { + throw error; + } + } + await this.fetchProfile(); + } catch (error) { + this.status = 'error'; + this.error = + error instanceof Error + ? error.message + : 'Не удалось зарегистрироваться. Попробуйте снова.'; + throw error; + } finally { + if (this.status !== 'error') { + this.status = 'idle'; + } + } + }, + async fetchProfile() { + if (!this.token) return; + try { + const profile = await AuthModel.profile(); + const normalizedProfile = normalizeUserProfile(profile); + if (normalizedProfile) { + this.setUser(normalizedProfile); + } + return normalizedProfile ?? null; + } catch (error) { + console.error('Не удалось получить профиль пользователя', error); + return null; + } + }, + initialize() { + const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY); + if (storedToken) { + this.setToken(storedToken); + } + }, + logout() { + this.setToken(null); + this.setUser(null); + this.status = 'idle'; + this.error = null; + }, + applyAuthResponse(response: AuthResponse) { + const token = extractToken(response); + if (!token) { + throw new MissingTokenError(); + } + + this.setToken(token); + + const userSources = [ + response.user, + response.profile, + isPlainObject(response.data) ? response.data : undefined, + response, + ]; + + for (const source of userSources) { + const normalized = normalizeUserProfile(source); + if (normalized) { + this.setUser(normalized); + break; + } + } + }, + }, +}); diff --git a/ionic-client/src/store/index.ts b/ionic-client/src/store/index.ts index 564cd1d..f465614 100644 --- a/ionic-client/src/store/index.ts +++ b/ionic-client/src/store/index.ts @@ -1,5 +1,6 @@ import { useMainMenuStore } from "./mainMenu"; import { usePreloader } from "./preloader"; import { useZayavkaStore } from "./zayavka"; +import { useAuthStore } from "./auth"; -export { useMainMenuStore, usePreloader, useZayavkaStore }; +export { useMainMenuStore, usePreloader, useZayavkaStore, useAuthStore }; diff --git a/ionic-client/src/types/dto/auth.ts b/ionic-client/src/types/dto/auth.ts new file mode 100644 index 0000000..320db7e --- /dev/null +++ b/ionic-client/src/types/dto/auth.ts @@ -0,0 +1,18 @@ +export type AuthResponse = { + accessToken?: string; + access_token?: string; + token?: string; + jwt?: string; + data?: unknown; + user?: unknown; + profile?: unknown; + [key: string]: unknown; +}; + +export type UserProfile = { + id: string | number; + email?: string; + username?: string; + createdAt?: string; + updatedAt?: string; +}; diff --git a/ionic-client/src/types/dto/index.ts b/ionic-client/src/types/dto/index.ts index 7a75c08..a3e35a5 100644 --- a/ionic-client/src/types/dto/index.ts +++ b/ionic-client/src/types/dto/index.ts @@ -71,6 +71,7 @@ export type MaterialRequestDTO = { createdAt: string; updatedAt: string; id: number; + userId?: number | null; }; export type StoredMaterialRequestDTO = { @@ -79,3 +80,5 @@ export type StoredMaterialRequestDTO = { updatedAt: string; id: number; }; + +export type { AuthResponse, UserProfile } from './auth'; diff --git a/ionic-client/src/vite-env.d.ts b/ionic-client/src/vite-env.d.ts index 3d09319..5074b25 100644 --- a/ionic-client/src/vite-env.d.ts +++ b/ionic-client/src/vite-env.d.ts @@ -11,6 +11,15 @@ interface ImportMetaEnv { readonly VITE_PROTOCOL: string; readonly VITE_BASE_HOST: string; readonly VITE_PORT: string; + readonly VITE_USER_API_ORIGIN?: string; + readonly VITE_AUTH_API_ORIGIN?: string; + readonly VITE_USER_API_PREFIX?: string; + readonly VITE_AUTH_API_PREFIX?: string; + readonly VITE_USER_API_LOGIN_PATHS?: string; + readonly VITE_USER_API_REGISTER_PATHS?: string; + readonly VITE_USER_API_PROFILE_PATHS?: string; + readonly VITE_USER_API_CREDENTIALS?: string; + readonly VITE_USER_API_AUTH_SCHEME?: string; } interface ImportMeta { From f2fd13969c20581e6b1c9835f3fd00ddf12e85c2 Mon Sep 17 00:00:00 2001 From: DmPrkp Date: Sun, 9 Nov 2025 11:59:52 +0300 Subject: [PATCH 2/2] Fix unused catch binding in router guard --- ionic-client/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ionic-client/src/main.ts b/ionic-client/src/main.ts index 2da2efa..5df9389 100644 --- a/ionic-client/src/main.ts +++ b/ionic-client/src/main.ts @@ -99,7 +99,7 @@ router.beforeEach(async (to) => { if (!authStore.user) { try { await authStore.fetchProfile(); - } catch (error) { + } catch { return { name: "auth", params: { locale: localeParam },