itube-specs 0.0.443 → 0.0.445

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.
@@ -40,7 +40,8 @@
40
40
  import type { IPasswordForm } from '../../types';
41
41
  import { validateEmail } from '../../runtime';
42
42
  import { AuthorizationApiService } from '~/services/api/authorization.service';
43
- import { useReCaptcha } from 'vue-recaptcha-v3';
43
+
44
+ import { initRecaptcha, getRecaptchaToken } from '../../composables/use-recaptcha';
44
45
 
45
46
  const {t} = useI18n();
46
47
 
@@ -53,8 +54,6 @@ const error = ref({
53
54
  email: false,
54
55
  });
55
56
 
56
- const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
57
-
58
57
  const emit = defineEmits<{
59
58
  (eventName: 'login'): void
60
59
  }>()
@@ -65,7 +64,17 @@ function onLoginClick() {
65
64
 
66
65
  const loading = ref(false)
67
66
 
68
- const { setErrorState, snackbarTheme, snackbarText, showErrorSnack, resetSnackbar } = useSnackbar()
67
+ const { setErrorState, snackbarTheme, snackbarText, showErrorSnack, resetSnackbar } = useSnackbar();
68
+
69
+ // Загружаем reCAPTCHA при открытии формы (onMounted) — даёт Google время собрать данные о поведении
70
+ onMounted(async () => {
71
+ try {
72
+ await initRecaptcha();
73
+ console.log('reCAPTCHA готов для этой формы');
74
+ } catch (err) {
75
+ console.error('Ошибка инициализации reCAPTCHA:', err);
76
+ }
77
+ });
69
78
 
70
79
  async function submit() {
71
80
  resetSnackbar()
@@ -79,8 +88,7 @@ async function submit() {
79
88
 
80
89
  try {
81
90
  loading.value = true;
82
- await recaptchaLoaded();
83
- form.value.token = await executeRecaptcha('password_reset');
91
+ form.value.token = await getRecaptchaToken('password_reset');
84
92
  await useUser(AuthorizationApiService).password(form.value);
85
93
  useAuthPopup().closeAuthPopup();
86
94
  snackbarTheme.value = 'success';
@@ -75,7 +75,8 @@
75
75
  import type { IRegistrateForm } from '../../types';
76
76
  import { validateEmail, validatePassword, validateUsername } from '../../runtime';
77
77
  import { AuthorizationApiService } from '~/services/api/authorization.service';
78
- import { useReCaptcha } from 'vue-recaptcha-v3';
78
+
79
+ import { initRecaptcha, getRecaptchaToken } from '../../composables/use-recaptcha';
79
80
 
80
81
  defineProps<{
81
82
  additionalText?: string;
@@ -83,9 +84,6 @@ defineProps<{
83
84
 
84
85
  const { t } = useI18n();
85
86
 
86
- // Правильное использование composable для Vue 3
87
- const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
88
-
89
87
  const form = ref({
90
88
  username: '',
91
89
  email: '',
@@ -110,6 +108,15 @@ function onLoginClick() {
110
108
  const loading = ref(false);
111
109
 
112
110
  const { setErrorState, snackbarText, snackbarTheme, showErrorSnack, resetSnackbar } = useSnackbar();
111
+ // Загружаем reCAPTCHA при открытии формы (onMounted) — даёт Google время собрать данные о поведении
112
+ onMounted(async () => {
113
+ try {
114
+ await initRecaptcha();
115
+ console.log('reCAPTCHA готов для этой формы');
116
+ } catch (err) {
117
+ console.error('Ошибка инициализации reCAPTCHA:', err);
118
+ }
119
+ });
113
120
 
114
121
  function validateForm(): boolean {
115
122
  const { username, email, password_hash } = form.value;
@@ -131,28 +138,30 @@ function validateForm(): boolean {
131
138
 
132
139
  async function submit() {
133
140
  resetSnackbar();
134
- if (!validateForm()) {
135
- console.log('Form is invalid');
136
- return;
137
- }
141
+ if (!validateForm()) return;
138
142
 
139
143
  try {
140
144
  loading.value = true;
141
145
 
142
- await recaptchaLoaded();
143
- form.value.token = await executeRecaptcha('register');
146
+ // Уникальное действие для каждой формы
147
+ // здесь 'register', в других формах — 'password_reset' или 'contact_form'
148
+ form.value.token = await getRecaptchaToken('register');
144
149
 
145
150
  await useUser(AuthorizationApiService).register(form.value);
146
151
 
147
152
  useAuthPopup().closeAuthPopup();
148
153
  snackbarTheme.value = 'success';
149
154
  snackbarText.value = 'Registration success';
155
+
156
+ // ... успех
150
157
  } catch (err) {
151
158
  setErrorState(err);
152
- // Опционально: обработка ошибки reCAPTCHA (например, если загрузка не удалась)
153
- console.error('reCAPTCHA error:', err);
159
+ console.error('Ошибка reCAPTCHA или регистрации:', err);
154
160
  } finally {
155
161
  loading.value = false;
156
162
  }
157
163
  }
158
164
  </script>
165
+
166
+ <style scoped>
167
+ </style>
@@ -114,7 +114,8 @@
114
114
  import { reportFormsScheme } from '../../lib/report-forms-scheme';
115
115
  import { EReportFormsSubjects, validateEmail, validatePhone } from '../../runtime';
116
116
  import type { InputTypes, IReportForm, IReportRequest } from '../../types';
117
- import { useReCaptcha } from 'vue-recaptcha-v3';
117
+
118
+ import { initRecaptcha, getRecaptchaToken } from '../../composables/use-recaptcha';
118
119
 
119
120
  const reasonValue = ref('');
120
121
  const errorRadio = ref(false);
@@ -139,8 +140,6 @@ const loading = ref(false);
139
140
  const activeCategory = computed(() => reportFormsScheme.find((subItem) => subItem.subject === form.value.categoryName))
140
141
  const requiredFields = computed(() => activeCategory.value?.items?.filter(item => item.required).map(item => item.value));
141
142
 
142
- const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
143
-
144
143
  const { snackbarText, snackbarTheme, showErrorSnack, resetSnackbar } = useSnackbar();
145
144
 
146
145
  function resetForm() {
@@ -158,6 +157,16 @@ const emit = defineEmits<{
158
157
  (eventName: 'submit', eventValue: IReportRequest): IReportRequest
159
158
  }>()
160
159
 
160
+ // Загружаем reCAPTCHA при открытии формы (onMounted) — даёт Google время собрать данные о поведении
161
+ onMounted(async () => {
162
+ try {
163
+ await initRecaptcha();
164
+ console.log('reCAPTCHA готов для этой формы');
165
+ } catch (err) {
166
+ console.error('Ошибка инициализации reCAPTCHA:', err);
167
+ }
168
+ });
169
+
161
170
  async function submit() {
162
171
  resetSnackbar();
163
172
  errorRadio.value = false;
@@ -196,8 +205,7 @@ async function submit() {
196
205
  try {
197
206
  const baseURL = window.location.origin;
198
207
  loading.value = true;
199
- await recaptchaLoaded();
200
- form.value.token = await executeRecaptcha('contact_form');
208
+ form.value.token = await getRecaptchaToken('contact_form');
201
209
  form.value.url = `${baseURL}/videos/${reportedVideoCard.value.id}`;
202
210
 
203
211
  if (reasonValue.value) {
@@ -0,0 +1,75 @@
1
+ // composables/useRecaptcha.ts
2
+ import { useRuntimeConfig } from '#imports';
3
+
4
+ let recaptchaLoaded = false;
5
+ let siteKey: string | null = null;
6
+
7
+ async function loadRecaptchaScript(): Promise<void> {
8
+ // Всё только на клиенте (SSR-safe)
9
+ if (process.server) return;
10
+
11
+ if (recaptchaLoaded) return;
12
+
13
+ siteKey = useRuntimeConfig().public.recaptchaSiteKey;
14
+
15
+ if (!siteKey) {
16
+ console.warn('reCAPTCHA siteKey не задан — защита отключена');
17
+ return;
18
+ }
19
+
20
+ const useRecaptchaNet = true; // true = recaptcha.net (для регионов с блокировкой Google)
21
+ const domain = useRecaptchaNet ? 'recaptcha.net' : 'www.google.com';
22
+
23
+ const script = document.createElement('script');
24
+ script.src = `https://${domain}/recaptcha/api.js?render=${siteKey}&hl=ru`;
25
+ script.async = true;
26
+ script.defer = true;
27
+ document.head.appendChild(script);
28
+
29
+ // Polling до готовности grecaptcha (надёжно и без ошибок listener)
30
+ const maxAttempts = 100; // ~10 сек
31
+ let attempts = 0;
32
+
33
+ while (typeof (window as any).grecaptcha === 'undefined' || !(window as any).grecaptcha.execute) {
34
+ if (attempts >= maxAttempts) {
35
+ throw new Error('reCAPTCHA не загрузился (таймаут)');
36
+ }
37
+ await new Promise(resolve => setTimeout(resolve, 100));
38
+ attempts++;
39
+ }
40
+
41
+ recaptchaLoaded = true;
42
+
43
+ // Опционально: скрываем бейдж (если хотите глобально)
44
+ const style = document.createElement('style');
45
+ style.textContent = '.grecaptcha-badge { visibility: hidden !important; }';
46
+ document.head.appendChild(style);
47
+
48
+ console.log('reCAPTCHA v3 загружен глобально');
49
+ }
50
+
51
+ // Инициализация — вызывайте в onMounted формы (даёт время на мониторинг поведения)
52
+ export async function initRecaptcha(): Promise<void> {
53
+ if (process.server) return;
54
+ if (recaptchaLoaded || !siteKey) return;
55
+
56
+ await loadRecaptchaScript();
57
+ }
58
+
59
+ // Получение токена — вызывайте в submit
60
+ export async function getRecaptchaToken(action: string): Promise<string> {
61
+ if (process.server) {
62
+ throw new Error('reCAPTCHA работает только на клиенте');
63
+ }
64
+
65
+ if (!recaptchaLoaded) {
66
+ await loadRecaptchaScript();
67
+ }
68
+
69
+ if (!siteKey) {
70
+ throw new Error('reCAPTCHA siteKey не задан');
71
+ }
72
+
73
+ // @ts-ignore grecaptcha глобальный
74
+ return await grecaptcha.execute(siteKey, { action });
75
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "itube-specs",
3
3
  "type": "module",
4
- "version": "0.0.443",
4
+ "version": "0.0.445",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./types/index.d.ts",
7
7
  "scripts": {
@@ -49,7 +49,6 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@vueform/slider": "2.1.10",
52
- "@vueuse/core": "12.8.2",
53
- "vue-recaptcha-v3": "^2.0.1"
52
+ "@vueuse/core": "12.8.2"
54
53
  }
55
54
  }