@nitra/cap 7.0.0 → 7.1.1

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,132 @@
1
+ <template>
2
+ <div v-if="appleError" class="text-white text-red bg-red-1 q-pa-xs q-mb-md text-center">
3
+ {{ appleError }}
4
+ </div>
5
+ <q-btn
6
+ v-if="!isAndroid"
7
+ @click="signInWithApple()"
8
+ :loading="checkInProgress"
9
+ class="full-width bg-white"
10
+ padding="6px"
11
+ no-caps
12
+ unelevated
13
+ style="border: 1px solid #ddd">
14
+ <div class="row items-center full-width q-pl-xs text-grey-9">
15
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512">
16
+ <path
17
+ fill="#555555"
18
+ d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" />
19
+ </svg>
20
+ <div class="col">{{ label }}</div>
21
+ </div>
22
+ </q-btn>
23
+ </template>
24
+
25
+ <script setup>
26
+ import { Capacitor } from '@capacitor/core'
27
+ import { SignInWithApple } from '@capacitor-community/apple-sign-in'
28
+ import { ref } from 'vue'
29
+ import { checkToken } from '@nitra/vite-boot/token'
30
+ import tf from '@nitra/tfm'
31
+ import homeCheck from './njs/home-check.js'
32
+
33
+ const props = defineProps({
34
+ // apple signin options
35
+ id: { type: String, default: null, required: true },
36
+ verifyOnly: { type: Boolean, default: false }
37
+ })
38
+
39
+ // событие когда пользователь авторизован
40
+ const emit = defineEmits(['verify', 'authorized'])
41
+
42
+ const t = tf.bind({ tr: getTr() })
43
+
44
+ const isAndroid = Capacitor.getPlatform() === 'android'
45
+ const label = props.verifyOnly ? t`Прив’язати мій Apple` : t`Вхід через Apple`
46
+
47
+ // console.log('props.id', props.id)
48
+ const appleSignInOptions = {
49
+ clientId: props.id,
50
+ redirectURI: `https://${window.location.host}/not-used-with-popup`, // Не використовується з popup, але потрібно щоб було задано
51
+ // та й ще це повинен бути той самий домен, який вказаний в apple developer
52
+ // та й поточного домену де кнопка
53
+ scopes: 'email name', // в якості id будемо брати sub
54
+ state: 'origin:web', // Any string of your choice that you may use for some logic. It's optional and you may omit it.
55
+ usePopup: true, // Important if we want to capture the data apple sends on the client side.
56
+ nonce: 'nonce'
57
+ }
58
+ // лоадер
59
+ const checkInProgress = ref(false)
60
+
61
+ // APPLE
62
+ const appleError = ref('')
63
+
64
+ const authAppleUser = async identityToken => {
65
+ if (!identityToken) {
66
+ checkInProgress.value = false
67
+ return
68
+ }
69
+ // пользователь авторизован
70
+ try {
71
+ const respFromRun = await fetch('/auth/apple', {
72
+ method: 'POST',
73
+ headers: {
74
+ 'Content-Type': 'application/json'
75
+ },
76
+ body: JSON.stringify({
77
+ idToken: identityToken,
78
+ verifyOnly: props.verifyOnly
79
+ })
80
+ })
81
+
82
+ const res = await respFromRun.json()
83
+ if (props.verifyOnly) {
84
+ emit('verify', res)
85
+ return
86
+ }
87
+
88
+ if (res?.token) {
89
+ const check = checkToken(res.token)
90
+ if (check.result === 'ok') {
91
+ emit('authorized', check.decoded)
92
+ } else {
93
+ // Перенаправляємо користувача на home, якщо йому дозволено тільки home
94
+ homeCheck(res.token)
95
+
96
+ appleError.value = t`Користувачу ${check.userName} не дозволено входити`
97
+ }
98
+ } else {
99
+ appleError.value = t`Помилка Apple авторизації`
100
+ }
101
+ } catch (error) {
102
+ appleError.value = error
103
+ } finally {
104
+ checkInProgress.value = false
105
+ }
106
+ }
107
+
108
+ const signInWithApple = async () => {
109
+ try {
110
+ const signInWithAppleResponse = await SignInWithApple.authorize(appleSignInOptions)
111
+ console.log(signInWithAppleResponse)
112
+ const { identityToken } = signInWithAppleResponse
113
+ await authAppleUser(identityToken)
114
+ } catch (error) {
115
+ appleError.value = error
116
+ }
117
+ }
118
+
119
+ /**
120
+ * LOCALIZATION
121
+ * @returns {object} translations
122
+ */
123
+ function getTr() {
124
+ return {
125
+ 'Вхід через Apple': { ru: 'Вход через аккаунт Apple' },
126
+ 'Прив’язати мій Apple': { ru: 'Привязать мой Apple' },
127
+ 'Помилка Apple авторизації': { ru: 'Ошибка авторизации Apple' },
128
+ 'Користувачу ': { ru: 'Пользователю ' },
129
+ ' не дозволено входити': { ru: ' не разрешен вход' }
130
+ }
131
+ }
132
+ </script>
@@ -0,0 +1,120 @@
1
+ <template>
2
+ <div v-if="googleError" class="text-white text-red bg-red-1 q-pa-xs q-mb-md text-center">
3
+ {{ googleError }}
4
+ </div>
5
+ <q-btn
6
+ v-if="isGoogleAuthInitialized"
7
+ @click="signInWithGoogle()"
8
+ :loading="checkInProgress"
9
+ icon="fa-brands fa-google"
10
+ :label="t`Вхід через Google`"
11
+ class="full-width"
12
+ padding="sm md"
13
+ color="google"
14
+ no-caps />
15
+ </template>
16
+
17
+ <script setup>
18
+ import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth'
19
+ import { ref } from 'vue'
20
+ import { checkToken } from '@nitra/vite-boot/token'
21
+ import tf from '@nitra/tfm'
22
+ import homeCheck from './njs/home-check.js'
23
+
24
+ const googleError = ref('')
25
+
26
+ const props = defineProps({
27
+ verifyOnly: { type: Boolean, default: false }
28
+ })
29
+
30
+ // событие когда пользователь авторизован
31
+ const emit = defineEmits(['verify', 'authorized'])
32
+
33
+ const t = tf.bind({ tr: getTr() })
34
+
35
+ const checkInProgress = ref(false)
36
+ const isGoogleAuthInitialized = ref(false)
37
+ try {
38
+ GoogleAuth.init()
39
+ isGoogleAuthInitialized.value = true
40
+ } catch {
41
+ isGoogleAuthInitialized.value = false
42
+ }
43
+
44
+ /**
45
+ *
46
+ */
47
+ async function signInWithGoogle() {
48
+ checkInProgress.value = true
49
+ googleError.value = ''
50
+ try {
51
+ const response = await GoogleAuth.signIn()
52
+ const credential = response?.authentication?.idToken
53
+ if (!credential) {
54
+ checkInProgress.value = false
55
+ googleError.value = t`Помилка Google авторизації`
56
+ return
57
+ }
58
+ authGoogleUser(credential)
59
+ } catch {
60
+ checkInProgress.value = false
61
+ googleError.value = t`Помилка Google авторизації`
62
+ }
63
+ }
64
+
65
+ /**
66
+ *
67
+ * @param credential
68
+ */
69
+ async function authGoogleUser(credential) {
70
+ try {
71
+ const respFromRun = await fetch('/auth/google', {
72
+ method: 'POST',
73
+ headers: {
74
+ 'Content-Type': 'application/json'
75
+ },
76
+ body: JSON.stringify({
77
+ credential,
78
+ verifyOnly: props.verifyOnly
79
+ })
80
+ })
81
+
82
+ const res = await respFromRun.json()
83
+ if (props.verifyOnly) {
84
+ emit('verify', res)
85
+ return
86
+ }
87
+
88
+ if (res?.token) {
89
+ const check = checkToken(res.token)
90
+ if (check.result === 'ok') {
91
+ emit('authorized', check.decoded)
92
+ } else {
93
+ // Перенаправляємо користувача на home, якщо йому дозволено тільки home
94
+ homeCheck(res.token)
95
+
96
+ googleError.value = t`Користувачу ${check.userName} не дозволено входити`
97
+ }
98
+ } else {
99
+ googleError.value = t`Помилка Google авторизації`
100
+ }
101
+ } catch (error) {
102
+ googleError.value = error
103
+ } finally {
104
+ checkInProgress.value = false
105
+ }
106
+ }
107
+
108
+ /**
109
+ * LOCALIZATION
110
+ * @returns {object} translations
111
+ */
112
+ function getTr() {
113
+ return {
114
+ 'Помилка Google авторизації': { ru: 'Ошибка авторизации Google' },
115
+ 'Користувачу ': { ru: 'Пользователю ' },
116
+ ' не дозволено входити': { ru: ' не разрешен вход' },
117
+ 'Вхід через Google': { ru: 'Вход через аккаунт Google' }
118
+ }
119
+ }
120
+ </script>
@@ -0,0 +1,331 @@
1
+ <template>
2
+ <q-page class="flex flex-center bg-grey-3">
3
+ <q-card style="width: 300px">
4
+ <!-- лого -->
5
+ <div style="margin-top: -50px" class="text-center">
6
+ <q-avatar class="shadow-2" size="100px">
7
+ <img :src="props.logo" />
8
+ </q-avatar>
9
+ </div>
10
+
11
+ <!-- вход по логину/паролю -->
12
+ <q-card-section class="q-pb-none">
13
+ <q-form @submit="loginSubmit" class="full-width q-gutter-y-md" autocomplete="on">
14
+ <div v-if="loginError" class="text-white text-red bg-red-1 q-pa-xs text-center">{{ loginError }}</div>
15
+
16
+ <q-input id="login" v-model.trim="login" type="login" autocomplete="username" :label="t`Логін`" outlined>
17
+ <template #prepend>
18
+ <q-icon name="person" />
19
+ </template>
20
+ <template v-if="login && browserSupportsWebAuthn()" #append>
21
+ <q-btn @click="biometry(login, 'login')" padding="none" flat icon="fingerprint" />
22
+ </template>
23
+ </q-input>
24
+ <q-input
25
+ id="password"
26
+ v-model="password"
27
+ :type="isPwd ? 'password' : 'text'"
28
+ autocomplete="current-password"
29
+ :label="t`Пароль`"
30
+ outlined>
31
+ <template #prepend>
32
+ <q-icon name="vpn_key" />
33
+ </template>
34
+ <template #append>
35
+ <q-icon @click="isPwd = !isPwd" :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer" />
36
+ </template>
37
+ </q-input>
38
+
39
+ <q-btn
40
+ v-if="login && password && password.length > 4"
41
+ :loading="loginLoading"
42
+ type="submit"
43
+ :label="t`Увійти`"
44
+ class="full-width q-mt-md"
45
+ padding="md"
46
+ color="primary" />
47
+ </q-form>
48
+
49
+ <n-forgot-pass />
50
+ </q-card-section>
51
+
52
+ <!-- или -->
53
+ <q-card-section v-if="apple || google" class="row items-center q-gutter-x-md">
54
+ <q-separator class="col" />
55
+ <div>{{ t`АБО` }}</div>
56
+ <q-separator class="col" />
57
+ </q-card-section>
58
+
59
+ <q-card-section v-if="apple || google" class="q-gutter-y-md">
60
+ <!-- вход по email -->
61
+ <q-input
62
+ v-model="email"
63
+ type="email"
64
+ :label="t`Вхід через E-mail`"
65
+ debounce="300"
66
+ :rules="[checkEmail]"
67
+ :error-message="t`Помилковий формат email`"
68
+ :hint="email ? t`на цю адресу буде надіслано код` : undefined"
69
+ hide-bottom-space
70
+ outlined
71
+ autofocus>
72
+ <template #prepend>
73
+ <q-icon name="mail" />
74
+ </template>
75
+ <template v-if="email && checkEmail(email) && browserSupportsWebAuthn()" #append>
76
+ <q-btn @click="biometry(email)" padding="none" flat icon="fingerprint" />
77
+ </template>
78
+ </q-input>
79
+ <q-btn
80
+ v-if="email && checkEmail(email)"
81
+ @click="emailSubmit"
82
+ :label="t`Отримати код`"
83
+ :loading="emailLoading"
84
+ class="full-width q-mt-md"
85
+ padding="md"
86
+ color="primary" />
87
+
88
+ <!-- google логин -->
89
+ <div v-if="google">
90
+ <n-cap-login-google v-if="isNative" />
91
+ <n-login-google v-else :id="google" @authorized="authorized" />
92
+ </div>
93
+
94
+ <!-- apple логин -->
95
+ <div v-if="apple">
96
+ <n-cap-login-apple :id="apple" @authorized="authorized" />
97
+ </div>
98
+ </q-card-section>
99
+ </q-card>
100
+ </q-page>
101
+ </template>
102
+
103
+ <script setup>
104
+ import NCapLoginApple from './NCapLoginApple.vue'
105
+ import NLoginGoogle from './NLoginGoogle.vue'
106
+ import NCapLoginGoogle from './NCapLoginGoogle.vue'
107
+ import homeCheck from './njs/home-check.js'
108
+ import NForgotPass from './NForgotPass.vue'
109
+ import { startAuthentication, browserSupportsWebAuthn } from '@simplewebauthn/browser'
110
+ import { ref, computed } from 'vue'
111
+ import { useRouter } from 'vue-router'
112
+ import { date, Notify } from 'quasar'
113
+ import { Capacitor } from '@capacitor/core'
114
+ import { checkToken } from '@nitra/vite-boot/token'
115
+ import { autoLogin, defaultLogin } from '@nitra/vite-boot/login'
116
+ import tf from '@nitra/tfm'
117
+
118
+ const router = useRouter()
119
+
120
+ const props = defineProps({
121
+ logo: {
122
+ type: String,
123
+ default: 'https://cdn.jsdelivr.net/npm/@nitra/auth-components@2/assets/logo.jpeg'
124
+ },
125
+ google: {
126
+ type: String,
127
+ default: null
128
+ },
129
+ // apple signin options
130
+ apple: {
131
+ type: String,
132
+ default: null
133
+ }
134
+ })
135
+
136
+ // событие когда пользователь авторизован
137
+ const emit = defineEmits(['authorized'])
138
+ // Авто авторизація якщо у користувача є коректний токен
139
+ autoLogin()
140
+
141
+ const t = tf.bind({ tr: getTr() })
142
+
143
+ const isNative = ref(Capacitor.isNativePlatform())
144
+
145
+ // ВХОД ПО EMAIL
146
+ const email = ref(localStorage.getItem('email') || '')
147
+ const emailLowerCased = computed(() => email.value.toLowerCase())
148
+ const emailLoading = ref(false)
149
+
150
+ // валидный ли адрес
151
+ const checkEmail = email => {
152
+ if (!email) {
153
+ return true
154
+ }
155
+
156
+ if (email.length > 320) {
157
+ return false
158
+ }
159
+
160
+ const regex =
161
+ /^(([^\s"(),.:;<>@[\\\]]+(\.[^\s"(),.:;<>@[\\\]]+)*)|(".+"))@((\[(?:\d{1,3}\.){3}\d{1,3}])|(([\dA-Za-z\-]+\.)+[A-Za-z]{2,}))$/
162
+
163
+ return regex.test(String(email).toLowerCase().trim())
164
+ }
165
+
166
+ // получить код
167
+ const emailSubmit = async () => {
168
+ try {
169
+ emailLoading.value = true
170
+
171
+ const response = await fetch('/auth/otp-email', {
172
+ method: 'POST',
173
+ headers: {
174
+ 'Content-Type': 'application/json'
175
+ },
176
+ body: JSON.stringify({
177
+ email: emailLowerCased.value,
178
+ href: globalThis.location.origin + '/code'
179
+ })
180
+ })
181
+ const res = await response.json()
182
+
183
+ if (res.error) {
184
+ loginError.value = JSON.stringify(res.error)
185
+ } else {
186
+ localStorage.setItem('email', emailLowerCased.value)
187
+ localStorage.setItem('email-otp-date', date.addToDate(new Date(), { minutes: 30 }))
188
+ await router.push('/code')
189
+ }
190
+ } catch (error) {
191
+ loginError.value = error.message
192
+ } finally {
193
+ emailLoading.value = false
194
+ }
195
+ }
196
+
197
+ // ВХОД ПО ЛОГИНУ/ПАРОЛЮ
198
+ const login = ref(null)
199
+ const password = ref(null)
200
+ const isPwd = ref(true)
201
+ const loginLoading = ref(false)
202
+ const loginError = ref('')
203
+
204
+ // отправить логин/пароль
205
+ const loginSubmit = async () => {
206
+ try {
207
+ loginLoading.value = true
208
+ loginError.value = ''
209
+
210
+ const data = { login: login.value, pass: password.value }
211
+
212
+ const response = await fetch('/auth/send-login-pass', {
213
+ method: 'POST',
214
+ headers: {
215
+ 'Content-Type': 'application/json'
216
+ },
217
+ body: JSON.stringify(data)
218
+ })
219
+
220
+ const res = await response.json()
221
+
222
+ if (res.token) {
223
+ const check = checkToken(res.token)
224
+ if (check.result === 'ok') {
225
+ await defaultLogin(check.decoded, res.token)
226
+ } else {
227
+ // Перенаправляємо користувача на home, якщо йому дозволено тільки home
228
+ homeCheck(res.token)
229
+
230
+ loginError.value = t`Користувачу ${localStorage.getItem(
231
+ 'email'
232
+ )} не дозволено входити в дане програмне забезпечення`
233
+ }
234
+ } else {
235
+ if (JSON.stringify(res.error).includes('Incorrect auth')) {
236
+ loginError.value = t`Помилковий логін або пароль`
237
+ } else {
238
+ loginError.value = t`Помилка: ${JSON.stringify(res.error)}`
239
+ }
240
+ }
241
+ } catch (error) {
242
+ loginError.value = error.message
243
+ } finally {
244
+ loginLoading.value = false
245
+ }
246
+ }
247
+
248
+ /**
249
+ *
250
+ * @param jwt
251
+ */
252
+ function authorized(jwt) {
253
+ emit('authorized', jwt)
254
+ }
255
+
256
+ const biometry = async (user, type = 'email') => {
257
+ try {
258
+ // Отримуємо параметри реєстрації (зберігається challenge на сервері)
259
+ const resp = await fetch('/auth/generate-authentication-options', {
260
+ method: 'POST',
261
+ headers: {
262
+ 'Content-Type': 'application/json'
263
+ },
264
+ body: JSON.stringify({
265
+ user,
266
+ type
267
+ })
268
+ })
269
+ const authOptions = await resp.json()
270
+
271
+ if (!authOptions.challenge) {
272
+ Notify.create({
273
+ type: 'warning',
274
+ message: t`${t`У користувача "`}${user}" немає зареєстрованих біометричних даних`
275
+ })
276
+ return
277
+ }
278
+
279
+ // Pass the options to the authenticator and wait for a response
280
+ const authResp = await startAuthentication(authOptions)
281
+
282
+ // POST the response to the endpoint that calls
283
+ const verificationResp = await fetch('/auth/verify-authentication', {
284
+ method: 'POST',
285
+ headers: {
286
+ 'Content-Type': 'application/json'
287
+ },
288
+ body: JSON.stringify(authResp)
289
+ })
290
+
291
+ // Wait for the results of verification
292
+ const res = await verificationResp.json()
293
+
294
+ if (res.verified && res.token) {
295
+ const check = checkToken(res.token)
296
+ if (check.result === 'ok') {
297
+ await defaultLogin(check.decoded, res.token)
298
+ } else {
299
+ loginError.value = t`Користувачу ${user} не дозволено входити в дане програмне забезпечення`
300
+ }
301
+ } else {
302
+ loginError.value = t`Користувачу ${user} не дозволено входити в дане програмне забезпечення`
303
+ }
304
+ } catch (error) {
305
+ Notify.create({ type: 'negative', message: `${error}` })
306
+ }
307
+ }
308
+
309
+ /**
310
+ * LOCALIZATION
311
+ * @returns {object} translations
312
+ */
313
+ function getTr() {
314
+ return {
315
+ 'на цю адресу буде надіслано код': { ru: 'на этот адрес будет отправлен код' },
316
+ 'Помилковий формат email': { ru: 'Некорректный формат email' },
317
+ 'Помилковий логін або пароль': { ru: 'Неверный логин или пароль' },
318
+ 'Отримати код': { ru: 'Получить код' },
319
+ 'Помилка: ': { ru: 'Ошибка: ' },
320
+ АБО: { ru: 'ИЛИ' },
321
+ Увійти: { ru: 'Войти' },
322
+ Логін: { ru: 'Логин' },
323
+ Пароль: { ru: 'Пароль' },
324
+ 'У користувача "': { ru: 'У пользователя "' },
325
+ '" немає зареєстрованих біометричних даних': { ru: '" нет зарегистрированных биометрических данных' },
326
+ 'Вхід через E-mail': { ru: 'Вход через E-mail' },
327
+ 'Користувачу ': { ru: 'Пользователю ' },
328
+ ' не дозволено входити в дане програмне забезпечення': { ru: ' не разрешено входить в данное приложение' }
329
+ }
330
+ }
331
+ </script>
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <q-dialog v-model="dialog" @hide="$emit('close')">
3
+ <q-card :style="'min-width: 330px;' + (wide ? 'max-width: 990px;' : 'max-width:90vw; ')">
4
+ <!-- Шапка диалога -->
5
+ <q-card-section :class="'text-white bg-' + color">
6
+ <div class="row items-center">
7
+ <q-icon v-if="icon" :name="icon" size="md" class="q-mr-sm" />
8
+ <div class="col">
9
+ <div class="text-h6">
10
+ {{ title }}
11
+ </div>
12
+ <div v-if="subtitle" class="text-subtitle1 q-my-xs">
13
+ {{ subtitle }}
14
+ </div>
15
+ </div>
16
+
17
+ <!-- закрыть диалог -->
18
+ <q-btn v-if="noActions" v-close-popup icon="close" color="white" class="q-ml-sm" round flat />
19
+ </div>
20
+ </q-card-section>
21
+
22
+ <!-- Слот для содержимого -->
23
+ <q-card-section>
24
+ <slot />
25
+ </q-card-section>
26
+
27
+ <!-- Дії -->
28
+ <q-card-section v-if="!noActions" class="row q-pt-none">
29
+ <!-- підтвердити -->
30
+ <q-btn @click="$emit('confirm')" :label="button" :color="color" padding="sm md" class="col" />
31
+ <!-- відмінити -->
32
+ <q-btn v-close-popup @click="$emit('cancel')" :label="t`Відмінити`" class="col" flat />
33
+ </q-card-section>
34
+ </q-card>
35
+ </q-dialog>
36
+ </template>
37
+
38
+ <script setup>
39
+ import { computed } from 'vue'
40
+ import tf from '@nitra/tfm'
41
+
42
+ const t = tf.bind({ tr: getTr() })
43
+
44
+ const props = defineProps({
45
+ // состояние диалога
46
+ modelValue: {
47
+ type: Boolean,
48
+ default: null
49
+ },
50
+ // заголовок
51
+ title: {
52
+ type: String,
53
+ default: ''
54
+ },
55
+ // подзаголовок
56
+ subtitle: {
57
+ type: String,
58
+ default: ''
59
+ },
60
+ // иконка диалога
61
+ icon: {
62
+ type: String,
63
+ default: null
64
+ },
65
+ // цвет диалога
66
+ color: {
67
+ type: String,
68
+ default: 'primary'
69
+ },
70
+ // название кнопки подтверждения
71
+ button: {
72
+ type: String,
73
+ default: 'Подтвердить'
74
+ },
75
+ // название кнопки отмены
76
+ buttonCancel: {
77
+ type: String,
78
+ default: ''
79
+ },
80
+ // без кнопок (появится крестик закрытия диалога в шапке)
81
+ noActions: {
82
+ type: Boolean,
83
+ default: false
84
+ },
85
+ // широкий вариант
86
+ wide: {
87
+ type: Boolean,
88
+ default: false
89
+ }
90
+ })
91
+
92
+ const emit = defineEmits(['update:modelValue', 'confirm', 'cancel', 'close'])
93
+
94
+ // состояние диалога - активен / неактивен
95
+ const dialog = computed({
96
+ get() {
97
+ return props.modelValue
98
+ },
99
+ set(value) {
100
+ emit('update:modelValue', value)
101
+ }
102
+ })
103
+
104
+ /**
105
+ * LOCALIZATION
106
+ * @returns {object} translations
107
+ */
108
+ function getTr() {
109
+ return {
110
+ Відмінити: { ru: 'Отмена' }
111
+ }
112
+ }
113
+ </script>
@@ -0,0 +1,98 @@
1
+ <template>
2
+ <div>
3
+ <q-btn
4
+ @click="forgotPassword = true"
5
+ :label="t`Забули пароль?`"
6
+ color="primary"
7
+ padding="16px 2px"
8
+ class="full-width"
9
+ align="center"
10
+ no-caps
11
+ flat />
12
+
13
+ <!-- Диалог - подтверждение удаления категории -->
14
+ <n-dialog v-model="forgotPassword" @confirm="restorePassword" :title="t`Відновлення паролю`" :button="t`Відновити`">
15
+ <div v-if="forgotError" class="text-white text-red q-pa-xs text-center" style="background: #f002">
16
+ {{ forgotError }}
17
+ </div>
18
+ <q-input
19
+ v-model="forgotLogin"
20
+ :label="t`Ваш телефон або email`"
21
+ :rules="[val => !!val || t`вкажіть телефон чи email`]"
22
+ outlined
23
+ hide-bottom-space />
24
+ </n-dialog>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup>
29
+ import NDialog from './NDialog.vue'
30
+ import { Notify } from 'quasar'
31
+ import { ref } from 'vue'
32
+ import tf from '@nitra/tfm'
33
+
34
+ const t = tf.bind({ tr: getTr() })
35
+
36
+ const forgotPassword = ref(false)
37
+ const forgotLogin = ref('')
38
+ const forgotError = ref('')
39
+
40
+ const restorePassword = async () => {
41
+ forgotError.value = ''
42
+ try {
43
+ const response = await fetch('/auth/forgot-pass', {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json'
47
+ },
48
+ body: JSON.stringify({ login: forgotLogin.value })
49
+ })
50
+
51
+ const res = await response.json()
52
+
53
+ if (res.error) {
54
+ forgotError.value = t`Помилка: ${JSON.stringify(res.error)}`
55
+ return
56
+ }
57
+
58
+ if (!res.type) {
59
+ forgotError.value = t`Некоректно вказано email або номер телефону`
60
+ return
61
+ }
62
+
63
+ if (res.type === 'email') {
64
+ Notify.create({
65
+ type: 'positive',
66
+ message: t`Новий пароль надіслано на email ${forgotLogin.value}`
67
+ })
68
+ } else if (res.type === 'phone') {
69
+ Notify.create({
70
+ type: 'positive',
71
+ message: t`Новий пароль надіслано на номер ${forgotLogin.value}`
72
+ })
73
+ }
74
+
75
+ forgotPassword.value = false
76
+ } catch (error) {
77
+ forgotError.value = error
78
+ }
79
+ }
80
+
81
+ /**
82
+ * LOCALIZATION
83
+ * @returns {object} translations
84
+ */
85
+ function getTr() {
86
+ return {
87
+ 'Забули пароль?': { ru: 'Забыли пароль?' },
88
+ 'Відновлення паролю': { ru: 'Восстановление пароля' },
89
+ Відновити: { ru: 'Восстановить' },
90
+ 'Ваш телефон або email': { ru: 'Ваш телефон или email' },
91
+ 'вкажіть телефон чи email': { ru: 'укажите телефон или email' },
92
+ 'Помилка: ': { ru: 'Ошибка: ' },
93
+ 'Некоректно вказано email або номер телефону': { ru: 'Некорректно указан email или номер телефона' },
94
+ 'Новий пароль надіслано на email ': { ru: 'Новый пароль отправлен на email ' },
95
+ 'Новий пароль надіслано на номер ': { ru: 'Новый пароль отправлен на номер ' }
96
+ }
97
+ }
98
+ </script>
@@ -0,0 +1,114 @@
1
+ <template>
2
+ <div v-if="googleError" class="text-white text-red bg-red-1 q-pa-xs q-mb-md text-center">
3
+ {{ googleError }}
4
+ </div>
5
+ <div id="buttonDiv" />
6
+ </template>
7
+
8
+ <script setup>
9
+ import { ref } from 'vue'
10
+ import { checkToken } from '@nitra/vite-boot/token'
11
+ import tf from '@nitra/tfm'
12
+ import homeCheck from './njs/home-check.js'
13
+
14
+ const googleError = ref('')
15
+
16
+ const props = defineProps({
17
+ id: { type: String, default: null, required: true },
18
+ verifyOnly: { type: Boolean, default: false }
19
+ })
20
+
21
+ // событие когда пользователь авторизован
22
+ const emit = defineEmits(['verify', 'authorized'])
23
+
24
+ const t = tf.bind({ tr: getTr() })
25
+
26
+ let local = 'uk-UA'
27
+ if (import.meta.env.VITE_T || import.meta.env.VITE_TFM === 'ru') {
28
+ local = 'ru_RU'
29
+ }
30
+
31
+ // Завантажуємо скрипт Google асинхронно
32
+ const scriptTag = document.createElement('script')
33
+ scriptTag.src = 'https://accounts.google.com/gsi/client'
34
+ scriptTag.async = true
35
+ scriptTag.defer = true
36
+ // Та коли він завантажився ініціалізуємо його
37
+ scriptTag.addEventListener('load', () => {
38
+ window.google.accounts.id.initialize({
39
+ client_id: props.id,
40
+ callback: handleCredentialResponse
41
+ })
42
+ // рендеримо кнопку з атрибутами
43
+ window.google.accounts.id.renderButton(document.querySelector('#buttonDiv'), {
44
+ theme: 'filled_white',
45
+ size: 'large',
46
+ text: 'signin_with',
47
+ width: '270',
48
+ locale: local
49
+ })
50
+ })
51
+ document.head.append(scriptTag)
52
+
53
+ // Коли відповідь від кнопки прийшла
54
+ /**
55
+ *
56
+ * @param credentialResponse
57
+ */
58
+ async function handleCredentialResponse(credentialResponse) {
59
+ const credential = credentialResponse.credential
60
+ googleError.value = ''
61
+
62
+ if (!credentialResponse.clientId || !credential) {
63
+ googleError.value = t`Помилка Google авторизації`
64
+ return
65
+ }
66
+
67
+ try {
68
+ const respFromRun = await fetch('/auth/google', {
69
+ method: 'POST',
70
+ headers: {
71
+ 'Content-Type': 'application/json'
72
+ },
73
+ body: JSON.stringify({
74
+ credential,
75
+ verifyOnly: props.verifyOnly
76
+ })
77
+ })
78
+
79
+ const res = await respFromRun.json()
80
+ if (props.verifyOnly) {
81
+ emit('verify', res)
82
+ return
83
+ }
84
+
85
+ if (res?.token) {
86
+ const check = checkToken(res.token)
87
+ if (check.result === 'ok') {
88
+ emit('authorized', check.decoded)
89
+ } else {
90
+ // Перенаправляємо користувача на home, якщо йому дозволено тільки home
91
+ homeCheck(res.token)
92
+
93
+ googleError.value = t`Користувачу ${check.userName} не дозволено входити`
94
+ }
95
+ } else {
96
+ googleError.value = t`Помилка Google авторизації`
97
+ }
98
+ } catch (error) {
99
+ googleError.value = error
100
+ }
101
+ }
102
+
103
+ /**
104
+ * LOCALIZATION
105
+ * @returns {object} translations
106
+ */
107
+ function getTr() {
108
+ return {
109
+ 'Помилка Google авторизації': { ru: 'Ошибка авторизации Google' },
110
+ 'Користувачу ': { ru: 'Пользователю ' },
111
+ ' не дозволено входити': { ru: ' не разрешен вход' }
112
+ }
113
+ }
114
+ </script>
package/auth/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { default as NCapLoginApple } from './NCapLoginApple.vue'
2
+ export { default as NCapLoginGoogle } from './NCapLoginGoogle.vue'
3
+ export { default as NCapLoginMulti } from './NCapLoginMulti.vue'
4
+ export { default as NDialog } from './NDialog.vue'
5
+ export { default as NForgotPass } from './NForgotPass.vue'
6
+ export { default as NLoginGoogle } from './NLoginGoogle.vue'
@@ -0,0 +1,23 @@
1
+ import jwtDecode from '@nitra/jwt-decode'
2
+
3
+ /**
4
+ *
5
+ * @param jwtOrig
6
+ */
7
+ export default function (jwtOrig) {
8
+ const jwt = jwtDecode(jwtOrig)
9
+
10
+ // Якщо не дозволено жодної ролі, то не йдемо далі
11
+ if (jwt['https://hasura.io/jwt/claims']['x-hasura-allowed-roles'].length === 0) {
12
+ return
13
+ }
14
+
15
+ // Якщо користувачу дозволено тільки home, то редиректимо на home
16
+ if (
17
+ jwt['https://hasura.io/jwt/claims']['x-hasura-allowed-roles'].length === 1 &&
18
+ jwt['https://hasura.io/jwt/claims']['x-hasura-allowed-roles'][0] === 'home-user'
19
+ ) {
20
+ const domain = import.meta.env.VITE_DOMAIN || '.nitra.ai'
21
+ globalThis.location.href = `https://home${domain}/profile`
22
+ }
23
+ }
package/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './app-update'
2
2
  export * from './save-token'
3
+ export * from './auth'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cap",
3
- "version": "7.0.0",
3
+ "version": "7.1.1",
4
4
  "description": "Nitra capacitor components",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,10 +20,15 @@
20
20
  },
21
21
  "homepage": "https://github.com/nitra/cap",
22
22
  "dependencies": {
23
- "@capacitor-firebase/messaging": "^7.0.0",
24
- "@capacitor/core": "^7.0.1",
25
- "@capawesome/capacitor-app-update": "^7.0.0",
26
- "@firebase/app": "^0.10.18",
27
- "firebase": "^11.2.0"
23
+ "@capacitor-community/apple-sign-in": "^7.0.1",
24
+ "@capacitor-firebase/messaging": "^7.3.1",
25
+ "@capacitor/core": "^7.4.3",
26
+ "@capawesome/capacitor-app-update": "^7.0.1",
27
+ "@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4",
28
+ "@firebase/app": "^0.14.3",
29
+ "@nitra/tfm": "^2.4.0",
30
+ "@nitra/vite-boot": "^3.7.1",
31
+ "@simplewebauthn/browser": "^11.0.0",
32
+ "firebase": "^12.3.0"
28
33
  }
29
34
  }