fix(pwa,auth): serve manifest and icons from apps/web/public and prevent Firebase uninitialized crash
@@ -1,4 +1,5 @@
|
|||||||
import { jwtDecode } from 'jwt-decode'
|
import { jwtDecode } from 'jwt-decode'
|
||||||
|
import { getApps } from 'firebase/app'
|
||||||
import { getAuth, signInWithPopup, GoogleAuthProvider, signOut, onAuthStateChanged } from 'firebase/auth'
|
import { getAuth, signInWithPopup, GoogleAuthProvider, signOut, onAuthStateChanged } from 'firebase/auth'
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@@ -15,8 +16,6 @@ export const useAuth = () => {
|
|||||||
sameSite: 'lax'
|
sameSite: 'lax'
|
||||||
})
|
})
|
||||||
|
|
||||||
const isInitializing = useState('auth_initializing', () => true)
|
|
||||||
|
|
||||||
const user = computed<User | null>(() => {
|
const user = computed<User | null>(() => {
|
||||||
if (!token.value) return null
|
if (!token.value) return null
|
||||||
try {
|
try {
|
||||||
@@ -30,6 +29,10 @@ export const useAuth = () => {
|
|||||||
const isAdmin = computed(() => user.value?.role === 'admin')
|
const isAdmin = computed(() => user.value?.role === 'admin')
|
||||||
|
|
||||||
const googleSignIn = async () => {
|
const googleSignIn = async () => {
|
||||||
|
if (!getApps().length) {
|
||||||
|
alert('Ошибка: Firebase не настроен. Проверьте переменные окружения FIREBASE_API_KEY в Coolify.')
|
||||||
|
return
|
||||||
|
}
|
||||||
const auth = getAuth()
|
const auth = getAuth()
|
||||||
const provider = new GoogleAuthProvider()
|
const provider = new GoogleAuthProvider()
|
||||||
const result = await signInWithPopup(auth, provider)
|
const result = await signInWithPopup(auth, provider)
|
||||||
@@ -46,8 +49,10 @@ export const useAuth = () => {
|
|||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
try {
|
try {
|
||||||
const auth = getAuth()
|
if (getApps().length) {
|
||||||
await signOut(auth)
|
const auth = getAuth()
|
||||||
|
await signOut(auth)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Firebase] Sign out failed or not initialized', e)
|
console.warn('[Firebase] Sign out failed or not initialized', e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -58,37 +63,44 @@ export const useAuth = () => {
|
|||||||
|
|
||||||
const initAuth = () => {
|
const initAuth = () => {
|
||||||
if (process.server) return
|
if (process.server) return
|
||||||
|
if (!getApps().length) {
|
||||||
|
console.warn('[Firebase] Firebase App not initialized, skipping onAuthStateChanged.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const auth = getAuth()
|
try {
|
||||||
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
|
const auth = getAuth()
|
||||||
// If user is authenticated in Firebase but we lost our backend token, try to restore
|
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
|
||||||
if (firebaseUser && !token.value) {
|
// If user is authenticated in Firebase but we lost our backend token, try to restore
|
||||||
try {
|
if (firebaseUser && !token.value) {
|
||||||
const idToken = await firebaseUser.getIdToken()
|
try {
|
||||||
const res = await $fetch<{ token: string; user: User }>('/api/auth/google', {
|
const idToken = await firebaseUser.getIdToken()
|
||||||
method: 'POST',
|
const res = await $fetch<{ token: string; user: User }>('/api/auth/google', {
|
||||||
body: { idToken }
|
method: 'POST',
|
||||||
})
|
body: { idToken }
|
||||||
token.value = res.token
|
})
|
||||||
|
token.value = res.token
|
||||||
|
|
||||||
if (useRoute().path === '/login') {
|
if (useRoute().path === '/login') {
|
||||||
await navigateTo('/')
|
await navigateTo('/')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to restore backend auth from Firebase', e)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to restore backend auth from Firebase', e)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
isInitializing.value = false
|
|
||||||
})
|
onUnmounted(() => unsubscribe())
|
||||||
return unsubscribe
|
} catch (err) {
|
||||||
|
console.warn('[Firebase] Auth initialization skipped:', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token,
|
|
||||||
user,
|
user,
|
||||||
|
token,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isInitializing,
|
|
||||||
googleSignIn,
|
googleSignIn,
|
||||||
logout,
|
logout,
|
||||||
initAuth
|
initAuth
|
||||||
|
|||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||||
|
<rect width="32" height="32" rx="6" fill="#0f172a"/>
|
||||||
|
<text x="16" y="22" font-size="18" text-anchor="middle" fill="#3b82f6" font-family="system-ui">N</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 252 B |
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"id": "/",
|
||||||
|
"name": "XFlow",
|
||||||
|
"short_name": "XFlow",
|
||||||
|
"description": "XFlow Intelligent Unified Platform",
|
||||||
|
"lang": "ru",
|
||||||
|
"categories": ["productivity", "business"],
|
||||||
|
"theme_color": "#090A0F",
|
||||||
|
"background_color": "#090A0F",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/pwa-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/pwa-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/pwa-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"id": "/",
|
||||||
|
"name": "XFlow",
|
||||||
|
"short_name": "XFlow",
|
||||||
|
"description": "XFlow Intelligent Unified Platform",
|
||||||
|
"lang": "ru",
|
||||||
|
"categories": ["productivity", "business"],
|
||||||
|
"theme_color": "#090A0F",
|
||||||
|
"background_color": "#090A0F",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/pwa-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/pwa-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/pwa-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||||
|
<rect width="512" height="512" fill="#0f172a"/>
|
||||||
|
<text x="256" y="350" font-size="280" text-anchor="middle" fill="#3b82f6" font-family="system-ui" font-weight="bold">N</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 273 B |
|
After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
|
||||||
|
<rect width="192" height="192" rx="24" fill="#0f172a"/>
|
||||||
|
<text x="96" y="128" font-size="96" text-anchor="middle" fill="#3b82f6" font-family="system-ui" font-weight="bold">N</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 279 B |
|
After Width: | Height: | Size: 292 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||||
|
<rect width="512" height="512" rx="64" fill="#0f172a"/>
|
||||||
|
<text x="256" y="350" font-size="280" text-anchor="middle" fill="#3b82f6" font-family="system-ui" font-weight="bold">N</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 281 B |