@nitra/cap 6.1.3 → 7.1.0

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>
@@ -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/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@nitra/cap",
3
- "version": "6.1.3",
3
+ "version": "7.1.0",
4
4
  "description": "Nitra capacitor components",
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./index.js"
7
+ ".": "./index.js",
8
+ "./auth/*": "./auth/*"
8
9
  },
9
10
  "scripts": {
10
11
  "fix": "yarn dlx eslint --fix . && npx prettier --write . "
@@ -20,10 +21,18 @@
20
21
  },
21
22
  "homepage": "https://github.com/nitra/cap",
22
23
  "dependencies": {
23
- "@capacitor-firebase/messaging": "^6.1.0",
24
- "@capacitor/core": "^6.1.2",
25
- "@capawesome/capacitor-app-update": "^6.0.0",
26
- "@firebase/app": "^0.10.11",
27
- "firebase": "^10.13.2"
28
- }
24
+ "@capacitor-community/apple-sign-in": "^7.0.1",
25
+ "@capacitor-firebase/messaging": "^7.3.1",
26
+ "@capacitor/core": "^7.4.3",
27
+ "@capawesome/capacitor-app-update": "^7.0.1",
28
+ "@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4",
29
+ "@firebase/app": "^0.14.3",
30
+ "@nitra/tfm": "^2.4.0",
31
+ "@nitra/vite-boot": "^3.7.1",
32
+ "@simplewebauthn/browser": "^11.0.0",
33
+ "firebase": "^12.3.0"
34
+ },
35
+ "files": [
36
+ "auth"
37
+ ]
29
38
  }
@@ -1,23 +0,0 @@
1
- # Проверка и установка новой версии приложение в маркете
2
-
3
- ## API
4
-
5
- - [`updateApp(...)`](#updateapp)
6
-
7
- ### updateApp(...)
8
-
9
- ```typescript
10
- updateApp(isImmediate: boolean | undefined, isMandatory: boolean | undefined ) => Promise<void>
11
- ```
12
-
13
- Проверка и установка новой версии приложение в маркете
14
-
15
- Для **Android**, через [In-app updates](https://developer.android.com/guide/playcore/in-app-updates)]
16
- Для **IOS**, открываем App Store
17
-
18
- | Параметр | Тип | Описание |
19
- |-------------------|-----------|-------------------------------------------------------------------|
20
- | **`isImmediate`** | `boolean` | только Android, обновление "в полный" экран, по-умолчанию - false |
21
- | **`isMandatory`** | `boolean` | только Android, обязательно обновление, по-умолчанию - false |
22
-
23
- ---
@@ -1,49 +0,0 @@
1
- import { Capacitor } from '@capacitor/core'
2
- import {
3
- AppUpdate,
4
- AppUpdateAvailability,
5
- FlexibleUpdateInstallStatus,
6
- AppUpdateResultCode
7
- } from '@capawesome/capacitor-app-update'
8
-
9
- /**
10
- * Проверка наличия обновления, для Android установка обновления, IOS - открытие AppStore
11
- * @async
12
- * @function updateApp
13
- * @param {boolean} isImmediate - только Android, обновление "в полный" экран, по-умолчанию - false
14
- * @param {boolean} isMandatory - только Android, обязательно обновление, по-умолчанию - false
15
- */
16
- export async function updateApp(isImmediate, isMandatory) {
17
- if (!Capacitor.isNativePlatform() || !Capacitor.isPluginAvailable('AppUpdate')) {
18
- return
19
- }
20
- const { updateAvailability } = await AppUpdate.getAppUpdateInfo()
21
- if (updateAvailability !== AppUpdateAvailability.UPDATE_AVAILABLE) {
22
- return
23
- }
24
- if (Capacitor.getPlatform() === 'ios') {
25
- await AppUpdate.openAppStore()
26
- return
27
- }
28
- if (Capacitor.getPlatform() === 'android') {
29
- if (isImmediate) {
30
- const appUpdateResult = await AppUpdate.performImmediateUpdate()
31
- if (
32
- isMandatory &&
33
- (appUpdateResult.code === AppUpdateResultCode.CANCELED ||
34
- appUpdateResult.code === AppUpdateResultCode.NOT_ALLOWED)
35
- ) {
36
- await updateApp(isImmediate, isMandatory)
37
- }
38
- } else {
39
- AppUpdate.addListener('onFlexibleUpdateStateChange', flexibleUpdateState => {
40
- if (flexibleUpdateState.installStatus === FlexibleUpdateInstallStatus.DOWNLOADED) {
41
- AppUpdate.removeAllListeners()
42
- AppUpdate.completeFlexibleUpdate()
43
- }
44
- })
45
- await AppUpdate.startFlexibleUpdate()
46
- }
47
- return
48
- }
49
- }
package/index.js DELETED
@@ -1,2 +0,0 @@
1
- export * from './app-update'
2
- export * from './save-token'
@@ -1,121 +0,0 @@
1
- # Receiving and Saving User Push Tokens
2
-
3
- ## Description
4
-
5
- This library is designed to handle obtaining and saving Firebase push tokens on the client side. It supports both mobile platforms (via Capacitor) and web platforms. The main function, `saveToken`, retrieves a push token from Firebase, stores it locally, and sends it to your server.
6
-
7
- This library interacts with Firebase for token management and Capacitor to check the platform and permissions. It ensures that push tokens are properly handled, regardless of whether the platform is iOS, Android, or Web.
8
-
9
- ## API
10
-
11
- ### `saveToken()`
12
-
13
- ```typescript
14
- saveToken() => Promise<void>
15
- ```
16
-
17
- ## usage the library
18
-
19
- ### To use this package, you should add this env to your project where you want to provide push notifications
20
-
21
- `VITE_APP=test`
22
- `VITE_PROJECT_ID=test`
23
- `VITE_VAPID_KEY=test`
24
- `VITE_API_KEY=test`
25
- `VITE_AUTH_DOMAIN=test`
26
- `VITE_STORAGE_BUCKET=test`
27
- `VITE_MESSAGING_SENDER_ID=test`
28
- `VITE_APP_ID=test`
29
-
30
- ## example in node
31
-
32
- ### Using function after login
33
-
34
- ```jsx
35
- <template>
36
- <n-code :on-token-checked="saveToken" />
37
- </template>
38
-
39
- <script setup>
40
- import NCode from '@nitra/abie-components/auth/NCode.vue'
41
- import { saveToken } from '@nitra/cap'
42
-
43
- await saveToken()
44
- </script>
45
-
46
- <route lang="yaml">
47
- meta:
48
- layout: blank
49
- </route>
50
-
51
- ```
52
-
53
- ## Environment Variable Validation with Vite
54
-
55
- ### To ensure that the required environment variables are set and the sw.js file is added to the project, add the following to your vite.config.js
56
-
57
- ```typescript
58
- import { requireEnvVar } from '@nitra/vite-check-env'
59
- import { defineConfig, loadEnv } from 'vite'
60
- import { build } from 'esbuild'
61
-
62
- export default {
63
- plugins: [
64
- {
65
- name: 'nitra-service-worker-transform',
66
- apply: 'build',
67
- transformIndexHtml() {
68
- build({
69
- plugins: [env(loadEnv(mode, '.'))],
70
- format: 'esm',
71
- entryPoints: [path.join(process.cwd(), '../node_modules/@nitra/cap/save-token/sw.js')],
72
- outfile: path.join(process.cwd(), 'dist', 'sw.js')
73
- })
74
- }
75
- },
76
- requireEnvVar([
77
- 'VITE_VAPID_KEY',
78
- 'VITE_APP',
79
- 'VITE_API_KEY',
80
- 'VITE_AUTH_DOMAIN',
81
- 'VITE_PROJECT_ID',
82
- 'VITE_STORAGE_BUCKET',
83
- 'VITE_MESSAGING_SENDER_ID',
84
- 'VITE_APP_ID'
85
- ])
86
- ]
87
- }
88
-
89
- function env(props) {
90
- return {
91
- name: 'env',
92
- setup: build => {
93
- const options = build.initialOptions
94
- const define = options.define ?? {}
95
- for (const k in props) {
96
- define[`import.meta.env.${k}`] = JSON.stringify(props[k])
97
- }
98
- options.define = {
99
- ...define
100
- }
101
- }
102
- }
103
- }
104
- ```
105
-
106
- # For local test you can add this code
107
- if (process.env.npm_lifecycle_event === 'start-remote-dev') {
108
- proxy['^/n-push/.*'] = ''
109
- } else {
110
- proxy['^/n-push/.*'] = '<http://0.0.0.0:8090>'
111
- }
112
-
113
- /n-push/ - url of the notify project
114
-
115
- The `notify project` is a server-side component designed to handle API requests for saving user push tokens. It uses the Fastify framework and JWT-based security for authentication.
116
-
117
- ### Install the plugin in the project directory
118
-
119
- ```typescript
120
- yarn add -D @nitra/vite-check-env
121
- ```
@@ -1,100 +0,0 @@
1
- import { Capacitor } from '@capacitor/core'
2
- import { FirebaseMessaging } from '@capacitor-firebase/messaging'
3
- import { getToken } from '@nitra/vite-boot/token'
4
- import { initializeApp } from 'firebase/app'
5
-
6
- /**
7
- * Save the Firebase push token for a given app and platform.
8
- * @returns {Promise<void>}
9
- */
10
- export async function saveToken() {
11
- try {
12
- if (localStorage.getItem(`pushToken_${import.meta.env.VITE_APP}`)) {
13
- console.log('Push Token is already exists.')
14
-
15
- return
16
- }
17
-
18
- initializeApp({
19
- apiKey: import.meta.env.VITE_API_KEY,
20
- authDomain: import.meta.env.VITE_AUTH_DOMAIN,
21
- projectId: import.meta.env.VITE_PROJECT_ID,
22
- storageBucket: import.meta.env.VITE_STORAGE_BUCKET,
23
- messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID,
24
- appId: import.meta.env.VITE_APP_ID
25
- })
26
-
27
- if (Capacitor.isNativePlatform() && !Capacitor.isPluginAvailable('FirebaseMessaging')) {
28
- console.error('FirebaseMessaging plugin is not available')
29
-
30
- return
31
- }
32
-
33
- let permissions = await FirebaseMessaging.checkPermissions()
34
-
35
- if (permissions.receive !== 'granted') {
36
- console.warn('Permissions are not granted, try to request permissions...')
37
-
38
- let permissionsRequest = await FirebaseMessaging.requestPermissions()
39
-
40
- if (permissionsRequest.receive !== 'granted') {
41
- console.error('Permissions are not granted, exiting...')
42
-
43
- return
44
- }
45
- }
46
-
47
- const pushToken = Capacitor.isNativePlatform()
48
- ? await FirebaseMessaging.getToken() // For iOS/Android
49
- : await getWebPushToken()
50
-
51
- if (!pushToken?.token && typeof pushToken?.token !== 'string') {
52
- console.error('Token is undefined exiting...')
53
-
54
- return
55
- }
56
-
57
- await sendTokenToServer(pushToken.token)
58
- } catch (error) {
59
- console.error('Error while saving push token:', error)
60
- }
61
- }
62
-
63
- /**
64
- * Get the Firebase push token for web platform.
65
- * @returns {Promise<object>} Web Push token
66
- */
67
- async function getWebPushToken() {
68
- const serviceWorkerRegistration = await navigator.serviceWorker.register('sw.js', {
69
- type: 'module'
70
- })
71
-
72
- return FirebaseMessaging.getToken({
73
- vapidKey: import.meta.env.VITE_VAPID_KEY,
74
- serviceWorkerRegistration
75
- })
76
- }
77
-
78
- /**
79
- * Send the Firebase push token to the server.
80
- * @param {string} pushToken - The push token returned by Firebase Messaging.
81
- * @returns {Promise<void>}
82
- */
83
- async function sendTokenToServer(pushToken) {
84
- const response = await fetch('/n-push/save-user', {
85
- method: 'POST',
86
- headers: {
87
- 'Content-Type': 'application/json',
88
- authorization: `Bearer ${getToken()}`
89
- },
90
- body: JSON.stringify({
91
- platform: Capacitor.getPlatform(),
92
- app: import.meta.env.VITE_APP,
93
- token: pushToken
94
- })
95
- })
96
-
97
- if (response.ok) {
98
- localStorage.setItem(`pushToken_${import.meta.env.VITE_APP}`, pushToken)
99
- }
100
- }
package/save-token/sw.js DELETED
@@ -1,13 +0,0 @@
1
- import { getMessaging } from 'https://www.gstatic.com/firebasejs/10.13.2/firebase-messaging-sw.js'
2
- import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.13.2/firebase-app.js'
3
-
4
- const firebaseApp = initializeApp({
5
- apiKey: import.meta.env.VITE_API_KEY,
6
- authDomain: import.meta.env.VITE_AUTH_DOMAIN,
7
- projectId: import.meta.env.VITE_PROJECT_ID,
8
- storageBucket: import.meta.env.VITE_STORAGE_BUCKET,
9
- messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID,
10
- appId: import.meta.env.VITE_APP_ID
11
- })
12
-
13
- export const messaging = getMessaging(firebaseApp)