File size: 7,017 Bytes
cc276cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181

"use client";

import { useState, useCallback, useEffect } from 'react';
import { onAuthStateChanged, signInAnonymously, User as FirebaseUser, signOut as firebaseSignOut } from "firebase/auth";
import { doc, getDoc, onSnapshot, updateDoc, arrayUnion, collection, query, where, getDocs, writeBatch } from "firebase/firestore";
import { ref, set, serverTimestamp } from 'firebase/database';
import { cryptoService } from '@/lib/crypto-service';
import { fingerprintService } from '@/lib/fingerprint-service';
import type { User } from '@/lib/types';
import { useFirebase } from '@/contexts/firebase-context';
import { Capacitor } from '@capacitor/core';
import { Device } from '@capacitor/device';
import { useToast } from '@/hooks/use-toast';


export const useAuthCore = () => {
    const { auth, db, rtdb } = useFirebase();
    const { toast } = useToast();
    const addToast = useCallback((message: string, options?: { variant?: 'default' | 'destructive' }) => {
        toast({ title: message, variant: options?.variant });
    }, [toast]);
    
    const [currentUser, setCurrentUser] = useState<User | null>(null);
    const [authStatus, setAuthStatus] = useState<'loading' | 'onboarding' | 'recovery' | 'authenticated'>('loading');
    
    const handleAuthenticatedUser = useCallback(async (user: FirebaseUser) => {
        const userProfileRef = doc(db, 'users', user.uid);
        const userSnap = await getDoc(userProfileRef);

        if (userSnap.exists()) {
            localStorage.setItem('userId', user.uid); // Save userId for Capacitor
            await cryptoService.setupUserKeys(user.uid, rtdb);
            
            const fingerprint = await fingerprintService.generate();
            const existingData = userSnap.data();
            if (fingerprint && (!existingData.deviceFingerprints || !existingData.deviceFingerprints.includes(fingerprint))) {
                await updateDoc(userProfileRef, {
                    deviceFingerprints: arrayUnion(fingerprint)
                });
            }

            const unsub = onSnapshot(userProfileRef, (doc) => {
                if (doc.exists()) {
                    setCurrentUser({ uid: doc.id, ...doc.data() } as User);
                } else {
                    setCurrentUser(null);
                    setAuthStatus('onboarding');
                }
            });
            setAuthStatus('authenticated');
            return unsub;
        } else {
            setAuthStatus('onboarding');
        }
        return () => {};
    }, [db, rtdb]);

    const recoverAccount = useCallback(async (recoveryId: string): Promise<boolean> => {
        if (!auth.currentUser) {
            addToast("Authentication not initialized. Please wait and try again.", { variant: "destructive" });
            return false;
        }
        const newUid = auth.currentUser.uid;

        // 1. Find original UID from recoveryId
        const recoveryDocRef = doc(db, 'recovery', recoveryId);
        const recoverySnap = await getDoc(recoveryDocRef);
        if (!recoverySnap.exists()) {
            addToast("Invalid Recovery ID.", { variant: "destructive" });
            return false;
        }
        const originalUid = recoverySnap.data().uid;

        if (originalUid === newUid) {
            addToast("This account is already active on this device.", { variant: "default" });
            await handleAuthenticatedUser(auth.currentUser);
            return true;
        }
        
        addToast("Account found! Migrating to new device...");

        try {
            const batch = writeBatch(db);

            const originalProfileRef = doc(db, 'users', originalUid);
            const originalProfileSnap = await getDoc(originalProfileRef);
            if (!originalProfileSnap.exists()) {
                throw new Error("Original user profile not found. The account may be corrupted.");
            }
            const originalProfileData = originalProfileSnap.data() as User;
            const publicId = originalProfileData.publicId;

            const newProfileData = { ...originalProfileData, uid: newUid };
            batch.set(doc(db, 'users', newUid), newProfileData);
            batch.update(doc(db, 'publicIds', publicId), { uid: newUid });
            batch.update(recoveryDocRef, { uid: newUid });

            // To be safe, we are not deleting the old user document.
            // This prevents data loss if the user is logged in on another device.
            
            await batch.commit();

            addToast("Account recovered successfully!");
            await handleAuthenticatedUser(auth.currentUser);
            return true;

        } catch (error: any) {
            console.error("Account migration failed:", error);
            addToast(`Recovery failed: ${error.message}`, { variant: "destructive" });
            return false;
        }
    }, [auth, db, addToast, handleAuthenticatedUser]);


    useEffect(() => {
        let isMounted = true;
        const unsubscribe = onAuthStateChanged(auth, async (user) => {
            if (!isMounted) return;
            if (user) {
                await handleAuthenticatedUser(user);
            } else {
                try {
                    await signInAnonymously(auth);
                } catch (error) {
                    console.error("Anonymous sign-in failed:", error);
                    if (isMounted) setAuthStatus('recovery'); // Go to recovery if sign-in fails
                }
            }
        });

        return () => {
            isMounted = false;
            unsubscribe();
        };
    }, [auth, handleAuthenticatedUser]);
    
    const signOutUser = useCallback(async () => {
        const userToSignOut = auth.currentUser;
        if (!userToSignOut && !currentUser) {
            setAuthStatus('recovery');
            window.location.reload();
            return;
        };
        
        const uidToUpdate = currentUser?.uid || userToSignOut?.uid;

        try {
            if(uidToUpdate) {
                const presenceRef = ref(rtdb, `presence/${uidToUpdate}`);
                await set(presenceRef, { isOnline: false, lastSeen: serverTimestamp() });
            }
            await firebaseSignOut(auth);
        } catch (error) {
            console.error("Sign out error:", error);
        } finally {
            setCurrentUser(null);
            setAuthStatus('recovery');
            window.location.reload();
        }
    }, [auth, rtdb, currentUser]);

    const updateUserProfile = useCallback(async (profileData: Partial<Pick<User, 'displayName' | 'photoURL' | 'bio' | 'status' | 'privacySettings'>>) => {
        if (!currentUser) return;
        const userRef = doc(db, 'users', currentUser.uid);
        await updateDoc(userRef, profileData);
    }, [currentUser, db]);


    return {
        currentUser,
        authStatus,
        setAuthStatus,
        signOutUser,
        recoverAccount,
        updateUserProfile,
        auth,
        handleAuthenticatedUser,
    };
};