bake canonical build harness (deterministic /ship for forks) [saas]
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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}</>;
|
||||
}
|
||||
Reference in New Issue
Block a user