itube-specs 0.0.804 → 0.0.805

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ // Возрастная верификация: СЕЙЧАС ОТКЛЮЧЕНА (middleware сразу возвращает return).
2
+ // Логика проверки гео + статуса верификации с редиректом на форму сохранена ниже
3
+ // в закомментированном виде — раскомментировать при включении.
4
+ // import { isGeoMatch, getAgeValidationUrl, isAgeVerified } from '../runtime';
5
+ // import { navigateTo } from '#app';
6
+
7
+ export default defineNuxtRouteMiddleware(async (_to) => {
8
+ return;
9
+ // if (!import.meta.server) return;
10
+ // if (to.path.startsWith('/bff/age/verify')) return;
11
+ //
12
+ // const event = useRequestEvent();
13
+ //
14
+ // const config = useRuntimeConfig();
15
+ // if (!(await isGeoMatch(event, config))) return;
16
+ //
17
+ // const verified = await isAgeVerified(event, config);
18
+ //
19
+ // if (!verified) {
20
+ // const { url } = await getAgeValidationUrl(event, to.path, config);
21
+ // if (url) {
22
+ // return navigateTo(url, { external: true });
23
+ // }
24
+ // }
25
+ });
@@ -0,0 +1,35 @@
1
+ // Глобальный auth-middleware: при наличии jwtoken-куки подтягивает профиль в
2
+ // useState('profileData') один раз за сессию (если ещё не загружен). На 400/401
3
+ // считает токен невалидным и чистит куку. Работает и на SSR, и на клиенте.
4
+ export default defineNuxtRouteMiddleware(async () => {
5
+ const cookie = useCookie('jwtoken', { path: '/', secure: true, httpOnly: false, sameSite: 'lax' });
6
+ if (!cookie.value) return;
7
+
8
+ const profileData = useState<any>('profileData', () => null);
9
+ if (profileData.value) return;
10
+
11
+ const headers = import.meta.server ? useRequestHeaders(['cookie']) : {};
12
+
13
+ const runtimeConfig = useRuntimeConfig();
14
+ const fetchProfile = () => $fetch('/bff/authorization/get-profile', {
15
+ headers: {
16
+ ...headers,
17
+ 'x-domain': runtimeConfig.public.xDomain,
18
+ 'Content-Type': 'application/json',
19
+ },
20
+ });
21
+
22
+ try {
23
+ profileData.value = await fetchProfile();
24
+ } catch (error: any) {
25
+ if (error?.statusCode === 400 || error?.statusCode === 401) {
26
+ cookie.value = null;
27
+ if (import.meta.client) {
28
+ // Nuxt-кука не всегда удаляется на клиенте с нужным domain — дочищаем вручную
29
+ document.cookie = `jwtoken=; path=/; domain=${runtimeConfig.public.xDomain}; expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure`;
30
+ }
31
+ } else {
32
+ console.error('Error get profile:', error);
33
+ }
34
+ }
35
+ });
@@ -0,0 +1,27 @@
1
+ // Глобальный middleware: нормализует URL (path + query) и делает 301-редирект,
2
+ // если текущий адрес отличается от канонического. Пути из EXCLUDED_PATHS пропускаются.
3
+ import { normalizeUrl } from '../runtime';
4
+
5
+ const EXCLUDED_PATHS = [
6
+ '/confirm/registration',
7
+ '/authorization/confirm'
8
+ ];
9
+
10
+ export default defineNuxtRouteMiddleware((to) => {
11
+ if (EXCLUDED_PATHS.some((path) => to.path.includes(path))) return;
12
+
13
+ const normalized = normalizeUrl(to);
14
+
15
+ const samePath = normalized.path === to.path;
16
+ const sameQuery = JSON.stringify(normalized.query) === JSON.stringify(to.query);
17
+
18
+ if (samePath && sameQuery) return;
19
+
20
+ return navigateTo(
21
+ {
22
+ path: normalized.path,
23
+ query: normalized.query
24
+ },
25
+ { replace: true, redirectCode: 301 }
26
+ );
27
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.804",
4
+ "version": "0.0.805",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -52,6 +52,9 @@
52
52
  "utils/",
53
53
  "types/",
54
54
  "lib/",
55
+ "middleware/",
56
+ "plugins/",
57
+ "server/",
55
58
  "nuxt.config.ts"
56
59
  ],
57
60
  "devDependencies": {
@@ -0,0 +1,84 @@
1
+ // SSR-плагин: по ISO-коду клиента резолвит имя страны в useState('clientCountryName')
2
+ // для гео-заголовка. Полный список стран в payload не кладётся (его лениво догружает
3
+ // селектор языка на клиенте). Результат кэшируется в куку и в память процесса.
4
+ import type { ICountries, ISelectItem } from '../types';
5
+ import { isoCodeMap } from '../utils/iso-code-map';
6
+
7
+ const COOKIE_OPTS = { maxAge: 60 * 60 * 24 * 365, path: '/', secure: true, sameSite: 'lax' as const };
8
+
9
+ type CountriesCache = {
10
+ items: ISelectItem[];
11
+ nameByIso: Map<string, string>;
12
+ };
13
+
14
+ const cache = new Map<string, Promise<CountriesCache | null>>();
15
+
16
+ function buildCache(data: ICountries): CountriesCache {
17
+ const items: ISelectItem[] = [];
18
+ const nameByIso = new Map<string, string>();
19
+
20
+ for (const item of data.countries) {
21
+ const iso = item.isoCodes[0].toLowerCase();
22
+ items.push({
23
+ title: item.title,
24
+ value: item.title,
25
+ icon: isoCodeMap[iso] || iso,
26
+ key: iso,
27
+ });
28
+ for (const code of item.isoCodes) {
29
+ nameByIso.set(code.toLowerCase(), item.title);
30
+ }
31
+ }
32
+ return { items, nameByIso };
33
+ }
34
+
35
+ function getCountries(language: string, domain: string): Promise<CountriesCache | null> {
36
+ const key = `${language}|${domain}`;
37
+ let promise = cache.get(key);
38
+ if (!promise) {
39
+ promise = $fetch<ICountries>('/api/dictionaries/countries', {
40
+ headers: { 'x-language': language, 'x-domain': domain },
41
+ })
42
+ .then(data => (data?.countries?.length ? buildCache(data) : null))
43
+ .catch(() => {
44
+ cache.delete(key);
45
+ return null;
46
+ });
47
+ cache.set(key, promise);
48
+ }
49
+ return promise;
50
+ }
51
+
52
+ export default defineNuxtPlugin(async () => {
53
+ const event = useRequestEvent();
54
+ const code = (event?.context?.clientCountryCode as string | undefined) || '';
55
+
56
+ const countryName = useState<string>('clientCountryName', () => '');
57
+
58
+ // Резолвим только имя текущей страны для гео-заголовка. Полный список стран
59
+ // НЕ кладём в useState (не сериализуем в payload) — language.vue догружает
60
+ // его лениво на клиенте при открытии селектора.
61
+ if (!code) return;
62
+
63
+ const codeCookie = useCookie<string>('clientCountryNameCode', COOKIE_OPTS);
64
+ const nameCookie = useCookie<string>('clientCountryName', COOKIE_OPTS);
65
+
66
+ // Тёплая кука → имя уже известно, серверный фетч стран не нужен.
67
+ if (nameCookie.value && codeCookie.value === code) {
68
+ countryName.value = nameCookie.value;
69
+ return;
70
+ }
71
+
72
+ const language = useRequestHeaders(['x-language'])['x-language'] ?? '';
73
+ const domain = useRuntimeConfig().public.xDomain as string;
74
+
75
+ const result = await getCountries(language, domain);
76
+ if (!result) return;
77
+
78
+ const name = result.nameByIso.get(code.toLowerCase());
79
+ if (name) {
80
+ countryName.value = name;
81
+ nameCookie.value = name;
82
+ codeCookie.value = code;
83
+ }
84
+ });
@@ -0,0 +1,7 @@
1
+ // SSR-плагин: переносит ISO-код страны клиента (определён в server/middleware/country.ts)
2
+ // из event.context в useState('clientCountryCode'), чтобы он был доступен в приложении.
3
+ export default defineNuxtPlugin((_nuxtApp) => {
4
+ const event = useRequestEvent();
5
+ const code = event?.context?.clientCountryCode || '';
6
+ useState<string>('clientCountryCode', () => code);
7
+ });
@@ -0,0 +1,8 @@
1
+ // SSR-плагин: определяет мобильное устройство по User-Agent и кладёт результат
2
+ // в useState('isMobile'), чтобы разметка совпадала на сервере и клиенте.
3
+ import { isMobileDevice } from '../runtime';
4
+
5
+ export default defineNuxtPlugin(() => {
6
+ const ua = useRequestHeaders(['user-agent'])['user-agent'] ?? '';
7
+ useState<boolean>('isMobile', () => isMobileDevice(ua));
8
+ });
@@ -0,0 +1,17 @@
1
+ // Регистрирует app-wide статические сервисы (SiteDataService) как $services
2
+ // через useState('global-services'), чтобы они были доступны во всём приложении.
3
+ import { SiteDataService } from '../services/site-data.service';
4
+
5
+ export default defineNuxtPlugin(() => {
6
+ const services = useState('global-services', () => {
7
+ return {
8
+ [SiteDataService.getServiceName()]: SiteDataService,
9
+ };
10
+ });
11
+
12
+ return {
13
+ provide: {
14
+ services
15
+ }
16
+ };
17
+ });
@@ -0,0 +1,10 @@
1
+ import { cleanCategoryInfo, getUrlWithProxiedParams } from '../../../runtime';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
5
+ const url = getUrlWithProxiedParams(`${apiUrl}/v1/categories/by-name`, event);
6
+
7
+ const data = await cachedApiFetch(url, event, 300);
8
+
9
+ return cleanCategoryInfo(data);
10
+ });
@@ -0,0 +1,30 @@
1
+ import type { ICategoryCard, IMultiSuggest, IRawCategoryCard } from '../../../types';
2
+ import { cleanCategoryCard, getUrlWithProxiedParams } from '../../../runtime';
3
+
4
+ export default defineEventHandler(async (event): Promise<ICategoryCard[]> => {
5
+ const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
6
+
7
+ const suggestUrl = getUrlWithProxiedParams(`${apiUrl}/v1/search/multi-suggest`, event);
8
+ const categoriesUrl = getUrlWithProxiedParams(`${apiUrl}/v1/categories`, event);
9
+
10
+ const headers = getRequestHeaders(event);
11
+
12
+ const [suggestRaw, categoriesRaw] = await Promise.all([
13
+ apiFetch(suggestUrl, { method: 'GET', headers: headers as HeadersInit }),
14
+ apiFetch(categoriesUrl, { method: 'GET', headers: headers as HeadersInit }),
15
+ ]);
16
+
17
+ const suggestData = suggestRaw.data as IMultiSuggest;
18
+ const categoriesData = categoriesRaw.data as IRawCategoryCard[];
19
+
20
+ // Если нет категорий в suggestData — возвращаем пустой массив
21
+ if (!suggestData?.categories?.length) {
22
+ return [];
23
+ }
24
+
25
+ const categoriesResult = suggestData.categories.map(item => item?.title.replace(/<[^>]+>/g, ''));
26
+
27
+ return categoriesData
28
+ .filter(item => categoriesResult.includes(item.title))
29
+ .map(cleanCategoryCard);
30
+ });
@@ -0,0 +1,11 @@
1
+ import type { IRawCategoryCard } from '../../../types';
2
+ import { getUrlWithProxiedParams, convertCategoriesToFooter } from '../../../runtime';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
6
+ const url = getUrlWithProxiedParams(`${apiUrl}/v1/categories/top-random`, event);
7
+
8
+ const categories = await cachedApiFetch<Array<IRawCategoryCard>>(url, event);
9
+
10
+ return convertCategoriesToFooter(categories);
11
+ });
@@ -0,0 +1,11 @@
1
+ import { cleanCategoryCard, getUrlWithProxiedParams } from '../../../runtime';
2
+ import type { IRawCategoryCard } from '../../../types';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
6
+ const url = getUrlWithProxiedParams(`${apiUrl}/v1/categories/top-random`, event);
7
+
8
+ const categories = await cachedApiFetch<Array<IRawCategoryCard>>(url, event);
9
+
10
+ return categories.map(cleanCategoryCard);
11
+ });
@@ -0,0 +1,11 @@
1
+ import type { IRawCategoryCard } from '../../../types';
2
+ import { getUrlWithProxiedParams, convertCategoriesToChips } from '../../../runtime';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
6
+ const url = getUrlWithProxiedParams(`${apiUrl}/v1/categories/top-random`, event);
7
+
8
+ const categories = await cachedApiFetch<Array<IRawCategoryCard>>(url, event);
9
+
10
+ return convertCategoriesToChips(categories, 'categories');
11
+ });
@@ -0,0 +1,23 @@
1
+ import { cleanCategoryCard, getUrlWithProxiedParams } from '../../../runtime';
2
+ import type { IRawGroupCategories } from '../../../types';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
6
+ const url = getUrlWithProxiedParams(`${apiUrl}/v1/groups-categories/all`, event);
7
+
8
+ const data = await cachedApiFetch<{ items?: IRawGroupCategories[] }>(url, event, 600);
9
+
10
+ const items = data?.items;
11
+
12
+ return Array.isArray(items)
13
+ ? items.map((item: IRawGroupCategories) => ({
14
+ name: item.name,
15
+ title: item.title,
16
+ weight: item.weight,
17
+ icon: item.icon,
18
+ categories: Array.isArray(item.categories)
19
+ ? item.categories.map(cleanCategoryCard)
20
+ : []
21
+ }))
22
+ : [];
23
+ });
@@ -0,0 +1,22 @@
1
+ import type { IRawCategoryCard } from '../../../types';
2
+ import { cleanMiniCategoryCard, getUrlWithProxiedParams, groupObjectsByFirstLetter } from '../../../runtime';
3
+
4
+ export default defineEventHandler(async (event) => {
5
+ const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
6
+ const url = getUrlWithProxiedParams(`${apiUrl}/v1/categories`, event);
7
+
8
+ const categories = await cachedApiFetch<Array<IRawCategoryCard>>(url, event);
9
+
10
+ const { letter } = getQuery<{ letter?: string }>(event);
11
+
12
+ if (!letter) {
13
+ return [];
14
+ }
15
+
16
+ const lang = event.node.req.headers[ 'x-language' ] || 'en';
17
+ const prop = getCategoryProp(lang);
18
+
19
+ const filtered = groupObjectsByFirstLetter(categories, prop)[ letter.toUpperCase() ] ?? [];
20
+
21
+ return filtered.map(cleanMiniCategoryCard);
22
+ });
@@ -0,0 +1,20 @@
1
+ import type { IChipsItem, IRawCategoryCard } from '../../../types';
2
+ import {
3
+ convertStringArrayToChips,
4
+ getUrlWithProxiedParams,
5
+ groupCategoriesByFirstLetter,
6
+ } from '../../../runtime';
7
+
8
+ export default defineEventHandler(async (event): Promise<IChipsItem[]> => {
9
+ const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
10
+ const url = getUrlWithProxiedParams(`${apiUrl}/v1/categories`, event);
11
+
12
+ const categories = await cachedApiFetch<Array<IRawCategoryCard>>(url, event);
13
+
14
+ const lang = event.node.req.headers['x-language'] || 'en';
15
+ const prop = getCategoryProp(lang);
16
+
17
+ const items = groupCategoriesByFirstLetter(categories, prop, getCategorySortLang(lang));
18
+
19
+ return convertStringArrayToChips(Object.keys(items), 'categories/letter', 'categories/all');
20
+ });
@@ -0,0 +1,14 @@
1
+ import type { ICategoryCard, IRawCategoryCard } from '../../../types';
2
+ import { getUrlWithProxiedParams, groupCategoriesByFirstLetter } from '../../../runtime';
3
+
4
+ export default defineEventHandler(async (event): Promise<Record<string, Array<ICategoryCard>>> => {
5
+ const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
6
+ const url = getUrlWithProxiedParams(`${apiUrl}/v1/categories`, event);
7
+
8
+ const categories = await cachedApiFetch<Array<IRawCategoryCard>>(url, event);
9
+
10
+ const lang = event.node.req.headers['x-language'] || 'en';
11
+ const prop = getCategoryProp(lang);
12
+
13
+ return groupCategoriesByFirstLetter(categories, prop, getCategorySortLang(lang));
14
+ });
@@ -0,0 +1,8 @@
1
+ import { getUrlWithProxiedParams } from '../../../runtime';
2
+
3
+ export default defineEventHandler(async (event) => {
4
+ const apiUrl = process.env.NUXT_PUBLIC_API_DOMAIN;
5
+ const url = getUrlWithProxiedParams(`${apiUrl}/v1/categories/related-phrases-by-name`, event);
6
+
7
+ return await cachedApiFetch(url, event, 600);
8
+ });