125 lines
4.1 KiB
TypeScript
125 lines
4.1 KiB
TypeScript
import type { Session, User } from '@supabase/supabase-js';
|
|
import { auth, crypto } from '@chat-app/shared';
|
|
import type { DeviceRecord } from '@chat-app/shared/auth';
|
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
|
import { Platform } from 'react-native';
|
|
|
|
import { secretStore } from './secretStore';
|
|
import { supabase } from './supabase';
|
|
|
|
// Locally-stored secrets keyed by stable names. Mirrors the desktop
|
|
// convention so the migration tests (later) can compare snapshots.
|
|
const KEY_DEVICE_ID = 'device.id';
|
|
const KEY_DEVICE_PRIVKEY = 'device.privateKey';
|
|
|
|
interface AuthContextValue {
|
|
session: Session | null;
|
|
user: User | null;
|
|
device: DeviceRecord | null;
|
|
ownPrivateKey: Uint8Array | null;
|
|
loading: boolean;
|
|
signOut: () => Promise<void>;
|
|
}
|
|
|
|
const Ctx = createContext<AuthContextValue | null>(null);
|
|
|
|
export function useAuth(): AuthContextValue {
|
|
const v = useContext(Ctx);
|
|
if (!v) throw new Error('useAuth() called outside <AuthProvider>');
|
|
return v;
|
|
}
|
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
const [session, setSession] = useState<Session | null>(null);
|
|
const [device, setDevice] = useState<DeviceRecord | null>(null);
|
|
const [ownPrivateKey, setOwnPrivateKey] = useState<Uint8Array | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Resolve or create the device record for this install given an active
|
|
// session. Stores the private key in expo-secure-store on first run.
|
|
const ensureDevice = useCallback(async (currentSession: Session): Promise<void> => {
|
|
const userId = currentSession.user.id;
|
|
const savedDeviceId = await secretStore.getSecret(KEY_DEVICE_ID);
|
|
const savedPrivKey = await secretStore.getSecret(KEY_DEVICE_PRIVKEY);
|
|
|
|
if (savedDeviceId && savedPrivKey) {
|
|
const devices = await auth.listOwnDevices(supabase);
|
|
const deviceIdStr = new TextDecoder().decode(savedDeviceId);
|
|
const match = devices.find((d) => d.id === deviceIdStr);
|
|
if (match) {
|
|
setDevice(match);
|
|
setOwnPrivateKey(savedPrivKey);
|
|
return;
|
|
}
|
|
// Stored id no longer matches any device on the server (revoked,
|
|
// wiped). Fall through to register a fresh one.
|
|
}
|
|
|
|
const backend = crypto.getCryptoBackend();
|
|
const kp = backend.generateKeyPair();
|
|
const platform = Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'linux';
|
|
const record = await auth.registerDevice(supabase, {
|
|
name: 'Netralax Mobile (' + Platform.OS + ')',
|
|
platform,
|
|
publicKey: kp.publicKey,
|
|
});
|
|
await secretStore.setSecret(KEY_DEVICE_ID, new TextEncoder().encode(record.id));
|
|
await secretStore.setSecret(KEY_DEVICE_PRIVKEY, kp.privateKey);
|
|
setDevice(record);
|
|
setOwnPrivateKey(kp.privateKey);
|
|
void userId;
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
void (async () => {
|
|
const { data } = await supabase.auth.getSession();
|
|
if (cancelled) return;
|
|
setSession(data.session);
|
|
if (data.session) {
|
|
try {
|
|
await ensureDevice(data.session);
|
|
} catch (err) {
|
|
console.warn('[auth] ensureDevice failed', err);
|
|
}
|
|
}
|
|
setLoading(false);
|
|
})();
|
|
|
|
const { data: sub } = supabase.auth.onAuthStateChange((_event, nextSession) => {
|
|
setSession(nextSession);
|
|
if (!nextSession) {
|
|
setDevice(null);
|
|
setOwnPrivateKey(null);
|
|
} else {
|
|
void ensureDevice(nextSession).catch((err) =>
|
|
console.warn('[auth] ensureDevice (state change) failed', err),
|
|
);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
sub.subscription.unsubscribe();
|
|
};
|
|
}, [ensureDevice]);
|
|
|
|
const signOut = useCallback(async (): Promise<void> => {
|
|
await supabase.auth.signOut();
|
|
await secretStore.removeSecret(KEY_DEVICE_ID);
|
|
await secretStore.removeSecret(KEY_DEVICE_PRIVKEY);
|
|
setDevice(null);
|
|
setOwnPrivateKey(null);
|
|
}, []);
|
|
|
|
const value: AuthContextValue = {
|
|
session,
|
|
user: session?.user ?? null,
|
|
device,
|
|
ownPrivateKey,
|
|
loading,
|
|
signOut,
|
|
};
|
|
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
|
}
|