@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.
- package/auth/NCapLoginApple.vue +132 -0
- package/auth/NCapLoginGoogle.vue +120 -0
- package/auth/NCapLoginMulti.vue +331 -0
- package/auth/NDialog.vue +113 -0
- package/auth/NForgotPass.vue +98 -0
- package/auth/NLoginGoogle.vue +114 -0
- package/auth/index.js +6 -0
- package/auth/njs/home-check.js +23 -0
- package/index.js +1 -0
- package/package.json +11 -6
|
@@ -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>
|
package/auth/NDialog.vue
ADDED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cap",
|
|
3
|
-
"version": "7.
|
|
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-
|
|
24
|
-
"@capacitor/
|
|
25
|
-
"@
|
|
26
|
-
"@
|
|
27
|
-
"
|
|
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
|
}
|