170 lines
4.6 KiB
TypeScript
170 lines
4.6 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import pb from '../lib/pocketbase';
|
|
import { trackEvent } from '../lib/analytics';
|
|
|
|
export interface AuthUser {
|
|
id: string;
|
|
email: string;
|
|
verified: boolean;
|
|
isAdmin: boolean;
|
|
subscription: string;
|
|
newsletter: boolean;
|
|
}
|
|
|
|
function recordToUser(record: { id: string; [key: string]: unknown }): AuthUser {
|
|
if (typeof record.email !== 'string') {
|
|
throw new Error('PocketBase record missing email field');
|
|
}
|
|
return {
|
|
id: record.id,
|
|
email: record.email,
|
|
verified: typeof record.verified === 'boolean' ? record.verified : false,
|
|
isAdmin: typeof record.is_admin === 'boolean' ? record.is_admin : false,
|
|
subscription: typeof record.subscription === 'string' ? record.subscription : 'free',
|
|
newsletter: typeof record.newsletter === 'boolean' ? record.newsletter : false,
|
|
};
|
|
}
|
|
|
|
export function useAuth() {
|
|
const [user, setUser] = useState<AuthUser | null>(() => {
|
|
if (pb.authStore.isValid && pb.authStore.record) {
|
|
return recordToUser(pb.authStore.record);
|
|
}
|
|
return null;
|
|
});
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Sync with authStore changes (cross-tab, external updates)
|
|
useEffect(() => {
|
|
const unsubscribe = pb.authStore.onChange(() => {
|
|
if (pb.authStore.isValid && pb.authStore.record) {
|
|
setUser(recordToUser(pb.authStore.record));
|
|
} else {
|
|
setUser(null);
|
|
}
|
|
});
|
|
return unsubscribe;
|
|
}, []);
|
|
|
|
const login = useCallback(async (email: string, password: string) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const result = await pb.collection('users').authWithPassword(email, password);
|
|
setUser(recordToUser(result.record));
|
|
trackEvent('Login', { method: 'email' });
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : 'Login failed';
|
|
setError(msg);
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const register = useCallback(async (email: string, password: string) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await pb.collection('users').create({
|
|
email,
|
|
password,
|
|
passwordConfirm: password,
|
|
});
|
|
// Auto-login after registration
|
|
const result = await pb.collection('users').authWithPassword(email, password);
|
|
setUser(recordToUser(result.record));
|
|
trackEvent('Register');
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : 'Registration failed';
|
|
setError(msg);
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const loginWithOAuth = useCallback(async (provider: string) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const result = await pb.collection('users').authWithOAuth2({ provider });
|
|
setUser(recordToUser(result.record));
|
|
trackEvent('Login', { method: provider });
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : 'OAuth login failed';
|
|
setError(msg);
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const logout = useCallback(() => {
|
|
trackEvent('Logout');
|
|
pb.authStore.clear();
|
|
setUser(null);
|
|
}, []);
|
|
|
|
const requestPasswordReset = useCallback(async (email: string) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await pb.collection('users').requestPasswordReset(email);
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : 'Password reset request failed';
|
|
setError(msg);
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const refreshAuth = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const result = await pb.collection('users').authRefresh();
|
|
setUser(recordToUser(result.record));
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : 'Auth refresh failed';
|
|
setError(msg);
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const requestVerification = useCallback(async (email: string) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
await pb.collection('users').requestVerification(email);
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : 'Verification request failed';
|
|
setError(msg);
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const clearError = useCallback(() => {
|
|
setError(null);
|
|
}, []);
|
|
|
|
return {
|
|
user,
|
|
loading,
|
|
error,
|
|
login,
|
|
register,
|
|
loginWithOAuth,
|
|
logout,
|
|
requestPasswordReset,
|
|
requestVerification,
|
|
refreshAuth,
|
|
clearError,
|
|
};
|
|
}
|