Files
template-saas/src/contexts/AuthContext.tsx
T

235 lines
7.6 KiB
TypeScript

// ---------------------------------------------------------------------------
// AuthContext — pre-built auth provider wired to AnyDB users collection
// ---------------------------------------------------------------------------
// PRE-INSTALLED in every AUTO_DB React project with auth: true.
// CodeWriterAgent just wraps App in <AuthProvider> and uses:
// import { useAuth } from "@/contexts/AuthContext"
// const { user, login, signup, logout, loading } = useAuth()
//
// Interface already knows:
// - users collection has: email (unique), name, avatar_url, plan, role, created_at
// - login: findOne by email + password hash
// - signup: insertOne with default plan='free'
// - session: localStorage token with user object
// ---------------------------------------------------------------------------
import React, { createContext, useContext, useState, useEffect, useCallback } from "react";
import anydb from "@/lib/anydb";
// Toast helper — uses window alert as fallback if useToast not available
const notify = (title: string, desc?: string) => {
try {
// Try to dispatch a custom event that a Toaster component can pick up
window.dispatchEvent(new CustomEvent('app-toast', { detail: { title, description: desc } }));
} catch {
console.log(`[Auth] ${title}: ${desc || ''}`);
}
};
interface User {
_id: string;
email: string;
name: string;
avatar_url?: string;
plan?: string;
role?: string;
created_at?: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
error: string | null;
login: (email: string, password: string) => Promise<boolean>;
signup: (name: string, email: string, password: string) => Promise<boolean>;
logout: () => void;
updateProfile: (updates: Partial<User>) => Promise<boolean>;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
const AUTH_KEY = "anydb_auth_user";
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const toast = (opts: { title: string; description?: string; variant?: string }) => notify(opts.title, opts.description);
// Restore session from localStorage on mount
useEffect(() => {
try {
const stored = localStorage.getItem(AUTH_KEY);
if (stored) {
setUser(JSON.parse(stored));
}
} catch {
localStorage.removeItem(AUTH_KEY);
}
setLoading(false);
}, []);
const login = useCallback(async (email: string, password: string): Promise<boolean> => {
setLoading(true);
setError(null);
try {
const col = await anydb.collection("users");
const result = await col.find({ email: email.toLowerCase() }, { limit: 1 });
const found = result.data?.[0];
if (!found) {
setError("No account found with that email");
toast({ title: "Login failed", description: "No account found with that email", variant: "destructive" });
return false;
}
// Simple password check (in production, use bcrypt on server)
if (found.password_hash !== password && found.password !== password) {
setError("Incorrect password");
toast({ title: "Login failed", description: "Incorrect password", variant: "destructive" });
return false;
}
const { password_hash, password: _pw, ...safeUser } = found;
setUser(safeUser);
localStorage.setItem(AUTH_KEY, JSON.stringify(safeUser));
toast({ title: "Welcome back!", description: `Signed in as ${safeUser.name || safeUser.email}` });
return true;
} catch (err: any) {
setError(err.message || "Login failed");
toast({ title: "Error", description: "Something went wrong. Please try again.", variant: "destructive" });
return false;
} finally {
setLoading(false);
}
}, [toast]);
const signup = useCallback(async (name: string, email: string, password: string): Promise<boolean> => {
setLoading(true);
setError(null);
try {
const col = await anydb.collection("users");
// Check if email already exists
const existing = await col.find({ email: email.toLowerCase() }, { limit: 1 });
if (existing.data?.length > 0) {
setError("An account with that email already exists");
toast({ title: "Signup failed", description: "An account with that email already exists", variant: "destructive" });
return false;
}
// Create user
const newUser = await col.insertOne({
name,
email: email.toLowerCase(),
password_hash: password, // In production: hash with bcrypt on server
plan: "free",
role: "user",
avatar_url: "",
created_at: new Date().toISOString(),
});
const safeUser: User = {
_id: newUser._id || newUser.insertedId,
name,
email: email.toLowerCase(),
plan: "free",
role: "user",
created_at: new Date().toISOString(),
};
setUser(safeUser);
localStorage.setItem(AUTH_KEY, JSON.stringify(safeUser));
toast({ title: "Account created!", description: `Welcome, ${name}!` });
return true;
} catch (err: any) {
setError(err.message || "Signup failed");
toast({ title: "Error", description: "Could not create account. Please try again.", variant: "destructive" });
return false;
} finally {
setLoading(false);
}
}, [toast]);
const logout = useCallback(() => {
setUser(null);
localStorage.removeItem(AUTH_KEY);
toast({ title: "Signed out", description: "You have been signed out." });
}, [toast]);
const updateProfile = useCallback(async (updates: Partial<User>): Promise<boolean> => {
if (!user?._id) return false;
try {
const col = await anydb.collection("users");
await col.updateOne({ _id: user._id }, updates);
const updatedUser = { ...user, ...updates };
setUser(updatedUser);
localStorage.setItem(AUTH_KEY, JSON.stringify(updatedUser));
toast({ title: "Profile updated" });
return true;
} catch (err: any) {
toast({ title: "Error", description: "Could not update profile", variant: "destructive" });
return false;
}
}, [user, toast]);
return (
<AuthContext.Provider value={{
user,
loading,
error,
login,
signup,
logout,
updateProfile,
isAuthenticated: !!user,
}}>
{children}
</AuthContext.Provider>
);
}
/**
* Use auth state and actions.
*
* Usage:
* const { user, login, signup, logout, isAuthenticated, loading } = useAuth()
* if (!isAuthenticated) return <Navigate to="/login" />
* await login(email, password)
* await signup(name, email, password)
* logout()
*/
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
/**
* Protected route wrapper. Redirects to /login if not authenticated.
*
* Usage in App.tsx:
* <Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
*/
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
</div>
);
}
if (!isAuthenticated) {
// Redirect to login
window.location.href = "/login";
return null;
}
return <>{children}</>;
}