parent
cf616c1ed8
commit
5eea1e2d70
@ -0,0 +1,6 @@
|
||||
{
|
||||
"firestore": {
|
||||
"rules": "firestore.rules",
|
||||
"indexes": "firestore.indexes.json"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"indexes": [],
|
||||
"fieldOverrides": []
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
rules_version = '2';
|
||||
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
function isSignedIn() {
|
||||
return request.auth != null;
|
||||
}
|
||||
|
||||
match /equipos/{equipoId} {
|
||||
allow read, write: if isSignedIn();
|
||||
}
|
||||
|
||||
match /bitacora/{eventoId} {
|
||||
allow read: if isSignedIn();
|
||||
allow create: if isSignedIn();
|
||||
allow update, delete: if false;
|
||||
}
|
||||
|
||||
match /contratos/{contratoId} {
|
||||
allow read, write: if isSignedIn();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,13 @@
|
||||
import './App.css'
|
||||
import { AuthProvider } from './features/auth/AuthProvider'
|
||||
import { AppRouter } from './routes/AppRouter'
|
||||
|
||||
function App() {
|
||||
return <AppRouter />
|
||||
return (
|
||||
<AuthProvider>
|
||||
<AppRouter />
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { createContext } from 'react'
|
||||
import type { User } from 'firebase/auth'
|
||||
|
||||
export type AuthContextValue = {
|
||||
isConfigured: boolean
|
||||
loading: boolean
|
||||
signInWithEmail: (email: string, password: string) => Promise<void>
|
||||
signInWithMicrosoft: () => Promise<void>
|
||||
signOut: () => Promise<void>
|
||||
user: User | null
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextValue | null>(null)
|
||||
@ -0,0 +1,55 @@
|
||||
import { onAuthStateChanged } from 'firebase/auth'
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import type { User } from 'firebase/auth'
|
||||
|
||||
import { isFirebaseConfigured } from '../../lib/firebase/config'
|
||||
import {
|
||||
getFirebaseAuth,
|
||||
signInWithEmail as firebaseSignInWithEmail,
|
||||
signInWithMicrosoft as firebaseSignInWithMicrosoft,
|
||||
signOutUser,
|
||||
} from '../../lib/firebase/auth'
|
||||
import { AuthContext, type AuthContextValue } from './AuthContext'
|
||||
|
||||
type AuthProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const isConfigured = isFirebaseConfigured()
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [loading, setLoading] = useState(() => isFirebaseConfigured())
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConfigured) {
|
||||
return
|
||||
}
|
||||
|
||||
const unsubscribe = onAuthStateChanged(getFirebaseAuth(), (nextUser) => {
|
||||
setUser(nextUser)
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [isConfigured])
|
||||
|
||||
const value = useMemo<AuthContextValue>(
|
||||
() => ({
|
||||
isConfigured,
|
||||
loading,
|
||||
async signInWithEmail(email, password) {
|
||||
await firebaseSignInWithEmail(email, password)
|
||||
},
|
||||
async signInWithMicrosoft() {
|
||||
await firebaseSignInWithMicrosoft()
|
||||
},
|
||||
async signOut() {
|
||||
await signOutUser()
|
||||
},
|
||||
user,
|
||||
}),
|
||||
[isConfigured, loading, user],
|
||||
)
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { FirebaseError } from 'firebase/app'
|
||||
|
||||
const AUTH_ERROR_MESSAGES: Record<string, string> = {
|
||||
'auth/configuration-not-found':
|
||||
'El proveedor de inicio de sesion no esta habilitado en Firebase.',
|
||||
'auth/invalid-credential': 'Usuario o contrasena incorrectos.',
|
||||
'auth/invalid-email': 'El correo no tiene un formato valido.',
|
||||
'auth/network-request-failed': 'No hay conexion. Revisa tu red e intenta de nuevo.',
|
||||
'auth/popup-blocked': 'El navegador bloqueo la ventana emergente. Permite popups e intenta de nuevo.',
|
||||
'auth/popup-closed-by-user': 'Se cancelo el inicio de sesion con Microsoft.',
|
||||
'auth/too-many-requests': 'Demasiados intentos fallidos. Espera un momento e intenta de nuevo.',
|
||||
'auth/user-disabled': 'Esta cuenta esta deshabilitada.',
|
||||
'auth/user-not-found': 'Usuario o contrasena incorrectos.',
|
||||
'auth/wrong-password': 'Usuario o contrasena incorrectos.',
|
||||
}
|
||||
|
||||
export function getFirebaseAuthErrorMessage(error: unknown): string {
|
||||
if (error instanceof FirebaseError) {
|
||||
return AUTH_ERROR_MESSAGES[error.code] ?? 'No se pudo completar el inicio de sesion.'
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
return 'Ocurrio un error inesperado.'
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { useContext } from 'react'
|
||||
|
||||
import { AuthContext } from './AuthContext'
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useAuth debe usarse dentro de AuthProvider.')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import type { FirebaseApp } from 'firebase/app'
|
||||
|
||||
export declare const firebaseConfig: {
|
||||
readonly apiKey: string
|
||||
readonly appId: string
|
||||
readonly authDomain: string
|
||||
readonly messagingSenderId: string
|
||||
readonly projectId: string
|
||||
readonly storageBucket: string
|
||||
}
|
||||
|
||||
export declare function isFirebaseConfigured(): boolean
|
||||
export declare function getFirebaseApp(): FirebaseApp
|
||||
@ -0,0 +1,34 @@
|
||||
import { getApp, getApps, initializeApp } from 'firebase/app'
|
||||
|
||||
/**
|
||||
* En Vite solo se exponen variables con prefijo VITE_ desde .env
|
||||
* @see https://vite.dev/guide/env-and-mode
|
||||
*/
|
||||
export const firebaseConfig = {
|
||||
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
||||
appId: import.meta.env.VITE_FIREBASE_APP_ID,
|
||||
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
|
||||
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
|
||||
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
||||
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
|
||||
}
|
||||
|
||||
export function isFirebaseConfigured() {
|
||||
return Object.values(firebaseConfig).every((value) => Boolean(value))
|
||||
}
|
||||
|
||||
let firebaseApp
|
||||
|
||||
export function getFirebaseApp() {
|
||||
if (!isFirebaseConfigured()) {
|
||||
throw new Error(
|
||||
'Firebase no esta configurado. Copia .env.example a .env y completa las variables VITE_FIREBASE_*.',
|
||||
)
|
||||
}
|
||||
|
||||
if (!firebaseApp) {
|
||||
firebaseApp = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig)
|
||||
}
|
||||
|
||||
return firebaseApp
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export { getFirebaseApp } from '../../firebase/firebase.js'
|
||||
@ -0,0 +1,43 @@
|
||||
import {
|
||||
OAuthProvider,
|
||||
getAuth,
|
||||
signInWithEmailAndPassword,
|
||||
signInWithPopup,
|
||||
signOut,
|
||||
type Auth,
|
||||
type User,
|
||||
} from 'firebase/auth'
|
||||
|
||||
import { getFirebaseApp } from './app'
|
||||
import { isFirebaseConfigured } from './config'
|
||||
|
||||
let authInstance: Auth | undefined
|
||||
|
||||
export function getFirebaseAuth(): Auth {
|
||||
if (!isFirebaseConfigured()) {
|
||||
throw new Error('Firebase Auth no esta disponible sin configuracion.')
|
||||
}
|
||||
|
||||
if (!authInstance) {
|
||||
authInstance = getAuth(getFirebaseApp())
|
||||
}
|
||||
|
||||
return authInstance
|
||||
}
|
||||
|
||||
export async function signInWithEmail(email: string, password: string): Promise<User> {
|
||||
const credential = await signInWithEmailAndPassword(getFirebaseAuth(), email, password)
|
||||
return credential.user
|
||||
}
|
||||
|
||||
export async function signInWithMicrosoft(): Promise<User> {
|
||||
const provider = new OAuthProvider('microsoft.com')
|
||||
provider.setCustomParameters({ prompt: 'select_account' })
|
||||
|
||||
const credential = await signInWithPopup(getFirebaseAuth(), provider)
|
||||
return credential.user
|
||||
}
|
||||
|
||||
export async function signOutUser(): Promise<void> {
|
||||
await signOut(getFirebaseAuth())
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
/** Nombres de colecciones raiz en Firestore para Nexus ECS. */
|
||||
export const firestoreCollections = {
|
||||
equipos: 'equipos',
|
||||
bitacora: 'bitacora',
|
||||
contratos: 'contratos',
|
||||
} as const
|
||||
|
||||
export type FirestoreCollectionName =
|
||||
(typeof firestoreCollections)[keyof typeof firestoreCollections]
|
||||
@ -0,0 +1 @@
|
||||
export { firebaseConfig, isFirebaseConfigured } from '../../firebase/firebase.js'
|
||||
@ -0,0 +1,42 @@
|
||||
import { connectFirestoreEmulator, getFirestore, type Firestore } from 'firebase/firestore'
|
||||
|
||||
import { getFirebaseApp } from './app'
|
||||
import { isFirebaseConfigured } from './config'
|
||||
|
||||
let firestoreInstance: Firestore | undefined
|
||||
let isEmulatorConnected = false
|
||||
|
||||
export function getFirestoreDb(): Firestore {
|
||||
if (!isFirebaseConfigured()) {
|
||||
throw new Error(
|
||||
'Firestore no esta disponible. Define las variables VITE_FIREBASE_* en un archivo .env.',
|
||||
)
|
||||
}
|
||||
|
||||
if (!firestoreInstance) {
|
||||
firestoreInstance = getFirestore(getFirebaseApp())
|
||||
connectFirestoreEmulatorIfConfigured(firestoreInstance)
|
||||
}
|
||||
|
||||
return firestoreInstance
|
||||
}
|
||||
|
||||
function connectFirestoreEmulatorIfConfigured(firestore: Firestore) {
|
||||
const emulatorHost = import.meta.env.VITE_FIREBASE_FIRESTORE_EMULATOR_HOST
|
||||
|
||||
if (!import.meta.env.DEV || !emulatorHost || isEmulatorConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
const [hostname, portValue] = emulatorHost.split(':')
|
||||
const port = Number(portValue)
|
||||
|
||||
if (!hostname || !Number.isFinite(port)) {
|
||||
throw new Error(
|
||||
'VITE_FIREBASE_FIRESTORE_EMULATOR_HOST debe tener el formato host:puerto (ej. 127.0.0.1:8080).',
|
||||
)
|
||||
}
|
||||
|
||||
connectFirestoreEmulator(firestore, hostname, port)
|
||||
isEmulatorConnected = true
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
|
||||
import { useAuth } from '../features/auth/useAuth'
|
||||
import { routes } from './paths'
|
||||
|
||||
type GuestRouteProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function GuestRoute({ children }: GuestRouteProps) {
|
||||
const { loading, user } = useAuth()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<main className="route-loading" aria-live="polite">
|
||||
<p>Cargando sesion...</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (user) {
|
||||
return <Navigate to={routes.dashboard} replace />
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
|
||||
import { useAuth } from '../features/auth/useAuth'
|
||||
import { routes } from './paths'
|
||||
|
||||
type ProtectedRouteProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
const { loading, user } = useAuth()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<main className="route-loading" aria-live="polite">
|
||||
<p>Cargando sesion...</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to={routes.login} replace />
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_FIREBASE_API_KEY: string
|
||||
readonly VITE_FIREBASE_AUTH_DOMAIN: string
|
||||
readonly VITE_FIREBASE_PROJECT_ID: string
|
||||
readonly VITE_FIREBASE_STORAGE_BUCKET: string
|
||||
readonly VITE_FIREBASE_MESSAGING_SENDER_ID: string
|
||||
readonly VITE_FIREBASE_APP_ID: string
|
||||
/** Solo desarrollo local. Formato: host:puerto (ej. 127.0.0.1:8080). */
|
||||
readonly VITE_FIREBASE_FIRESTORE_EMULATOR_HOST?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
Loading…
Reference in new issue