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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
}
|