Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 54 additions & 22 deletions ionic-client/src/components/nav/FooterBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
70 changes: 70 additions & 0 deletions ionic-client/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
return {
name: "auth",
params: { locale: localeParam },
query: { redirect: to.fullPath },
};
}
}

return true;
});

const app = createApp(App)
.use(IonicVue)
Expand All @@ -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(() => {
Expand Down
128 changes: 128 additions & 0 deletions ionic-client/src/models/AuthModel.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>
): Promise<AuthResponse> {
let lastError: unknown;

for (const path of paths) {
try {
return await this.post<AuthResponse>({
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<unknown>(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);
}
}
42 changes: 37 additions & 5 deletions ionic-client/src/models/BaseModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ export default class BaseModel {
},
};

static setAuthToken(token?: string, scheme = "Bearer") {
const headers = (this.baseOpts.headers || {}) as Record<string, string>;

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}`
Expand All @@ -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<R>(params: string): Promise<R | undefined> {
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);
Expand All @@ -41,8 +75,7 @@ export default class BaseModel {
queries?: string[];
opts?: RequestInit;
}): Promise<R> {
const queryString = queries.length ? `?${queries.join("&")}` : "";
const query = `${this.baseURL}${this.apiVersion}${params}${queryString}`;
const query = this.buildUrl(params, queries);

const options = Object.assign(
{
Expand Down Expand Up @@ -73,8 +106,7 @@ export default class BaseModel {
queries?: string[];
opts?: RequestInit;
}): Promise<R> {
const queryString = queries.length ? `?${queries.join("&")}` : "";
const query = `${this.baseURL}${this.apiVersion}${params}${queryString}`;
const query = this.buildUrl(params, queries);

const options = Object.assign(
{
Expand Down
Loading