Update authentication to use Supabase and enable user sign-up

Integrate Supabase Auth for user login and sign-up, replacing the previous custom authentication system. This change modifies API routes, frontend authentication context, and the login page to support email-based authentication and a new sign-up flow.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 7b6bc38e-d7e0-4263-881e-db7e4f1e15bb
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/bgcvGPx
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-17 02:13:28 +00:00
parent 091461a865
commit 0cfb38d847
7 changed files with 215 additions and 159 deletions

View file

@ -41,3 +41,9 @@ waitForPort = 5000
[agent] [agent]
mockupState = "FULLSTACK" mockupState = "FULLSTACK"
integrations = ["javascript_openai_ai_integrations:1.0.0"] integrations = ["javascript_openai_ai_integrations:1.0.0"]
[userenv]
[userenv.shared]
VITE_SUPABASE_URL = "${SUPABASE_URL}"
VITE_SUPABASE_ANON_KEY = "${SUPABASE_ANON_KEY}"

View file

@ -1,8 +1,9 @@
import { createContext, useContext, useState, useEffect, ReactNode } from "react"; import { createContext, useContext, ReactNode } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
interface User { interface User {
id: string; id: string;
email?: string;
username: string; username: string;
isAdmin: boolean; isAdmin: boolean;
} }
@ -12,7 +13,8 @@ interface AuthContextType {
isLoading: boolean; isLoading: boolean;
isAuthenticated: boolean; isAuthenticated: boolean;
isAdmin: boolean; isAdmin: boolean;
login: (username: string, password: string) => Promise<void>; login: (email: string, password: string) => Promise<void>;
signup: (email: string, password: string, username?: string) => Promise<{ message: string }>;
logout: () => Promise<void>; logout: () => Promise<void>;
} }
@ -21,26 +23,26 @@ const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) { export function AuthProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: session, isLoading, isFetching } = useQuery({ const { data: session, isLoading } = useQuery({
queryKey: ["session"], queryKey: ["session"],
queryFn: async () => { queryFn: async () => {
const res = await fetch("/api/auth/session", { credentials: "include" }); const res = await fetch("/api/auth/session", { credentials: "include" });
return res.json(); return res.json();
}, },
staleTime: 5 * 60 * 1000, // 5 minutes staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000, // 10 minutes gcTime: 10 * 60 * 1000,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false, refetchOnReconnect: false,
}); });
const loginMutation = useMutation({ const loginMutation = useMutation({
mutationFn: async ({ username, password }: { username: string; password: string }) => { mutationFn: async ({ email, password }: { email: string; password: string }) => {
const res = await fetch("/api/auth/login", { const res = await fetch("/api/auth/login", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
credentials: "include", credentials: "include",
body: JSON.stringify({ username, password }), body: JSON.stringify({ email, password }),
}); });
if (!res.ok) { if (!res.ok) {
const data = await res.json(); const data = await res.json();
@ -53,6 +55,22 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}, },
}); });
const signupMutation = useMutation({
mutationFn: async ({ email, password, username }: { email: string; password: string; username?: string }) => {
const res = await fetch("/api/auth/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ email, password, username }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Signup failed");
}
return res.json();
},
});
const logoutMutation = useMutation({ const logoutMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
await fetch("/api/auth/logout", { method: "POST", credentials: "include" }); await fetch("/api/auth/logout", { method: "POST", credentials: "include" });
@ -62,8 +80,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}, },
}); });
const login = async (username: string, password: string) => { const login = async (email: string, password: string) => {
await loginMutation.mutateAsync({ username, password }); await loginMutation.mutateAsync({ email, password });
};
const signup = async (email: string, password: string, username?: string) => {
return await signupMutation.mutateAsync({ email, password, username });
}; };
const logout = async () => { const logout = async () => {
@ -76,6 +98,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
isAuthenticated: !!session?.authenticated, isAuthenticated: !!session?.authenticated,
isAdmin: session?.user?.isAdmin || false, isAdmin: session?.user?.isAdmin || false,
login, login,
signup,
logout, logout,
}; };

View file

@ -0,0 +1,13 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
console.warn('Supabase credentials not found. Auth features may not work.');
}
export const supabase = createClient(
supabaseUrl || '',
supabaseAnonKey || ''
);

View file

@ -1,16 +1,18 @@
import { useState } from "react"; import { useState } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { Shield, Lock, AlertCircle } from "lucide-react"; import { Shield, Lock, AlertCircle, UserPlus } from "lucide-react";
import { useAuth } from "@/lib/auth"; import { useAuth } from "@/lib/auth";
import gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png'; import gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png';
export default function Login() { export default function Login() {
const [username, setUsername] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { login } = useAuth(); const [mode, setMode] = useState<'login' | 'signup'>('login');
const { login, signup } = useAuth();
const [, setLocation] = useLocation(); const [, setLocation] = useLocation();
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
@ -18,14 +20,22 @@ export default function Login() {
if (isLoading) return; if (isLoading) return;
setError(""); setError("");
setSuccess("");
setIsLoading(true); setIsLoading(true);
try { try {
await login(username, password); if (mode === 'login') {
await new Promise(resolve => setTimeout(resolve, 100)); await login(email, password);
setLocation("/admin"); await new Promise(resolve => setTimeout(resolve, 100));
setLocation("/admin");
} else {
const result = await signup(email, password);
setSuccess(result.message || "Account created! Please check your email to confirm.");
setMode('login');
}
} catch (err: any) { } catch (err: any) {
setError(err.message || "Login failed"); setError(err.message || `${mode === 'login' ? 'Login' : 'Signup'} failed`);
} finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
@ -50,10 +60,31 @@ export default function Login() {
AeThex Command AeThex Command
</h1> </h1>
<p className="text-muted-foreground text-sm mt-2"> <p className="text-muted-foreground text-sm mt-2">
Authorized Personnel Only {mode === 'login' ? 'Authorized Personnel Only' : 'Create Your Account'}
</p> </p>
</div> </div>
<div className="flex mb-6 border border-white/10 rounded overflow-hidden">
<button
type="button"
onClick={() => { setMode('login'); setError(''); setSuccess(''); }}
className={`flex-1 py-2 text-sm uppercase tracking-wider transition-colors ${
mode === 'login' ? 'bg-primary text-background' : 'bg-card text-muted-foreground hover:text-white'
}`}
>
Login
</button>
<button
type="button"
onClick={() => { setMode('signup'); setError(''); setSuccess(''); }}
className={`flex-1 py-2 text-sm uppercase tracking-wider transition-colors ${
mode === 'signup' ? 'bg-primary text-background' : 'bg-card text-muted-foreground hover:text-white'
}`}
>
Sign Up
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{error && ( {error && (
<div className="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/30 text-destructive text-sm" data-testid="error-message"> <div className="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/30 text-destructive text-sm" data-testid="error-message">
@ -62,17 +93,23 @@ export default function Login() {
</div> </div>
)} )}
{success && (
<div className="flex items-center gap-2 p-3 bg-green-500/10 border border-green-500/30 text-green-400 text-sm" data-testid="success-message">
{success}
</div>
)}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs text-muted-foreground uppercase tracking-widest"> <label className="text-xs text-muted-foreground uppercase tracking-widest">
Username Email
</label> </label>
<input <input
type="text" type="email"
value={username} value={email}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full bg-card border border-white/10 px-4 py-3 text-white placeholder-muted-foreground focus:border-primary/50 focus:outline-none transition-colors" className="w-full bg-card border border-white/10 px-4 py-3 text-white placeholder-muted-foreground focus:border-primary/50 focus:outline-none transition-colors"
placeholder="Enter username" placeholder="Enter email"
data-testid="input-username" data-testid="input-email"
required required
/> />
</div> </div>
@ -86,9 +123,10 @@ export default function Login() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full bg-card border border-white/10 px-4 py-3 text-white placeholder-muted-foreground focus:border-primary/50 focus:outline-none transition-colors" className="w-full bg-card border border-white/10 px-4 py-3 text-white placeholder-muted-foreground focus:border-primary/50 focus:outline-none transition-colors"
placeholder="Enter password" placeholder={mode === 'signup' ? 'Min 6 characters' : 'Enter password'}
data-testid="input-password" data-testid="input-password"
required required
minLength={mode === 'signup' ? 6 : undefined}
/> />
</div> </div>
@ -96,15 +134,20 @@ export default function Login() {
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className="w-full bg-primary text-background py-3 font-bold uppercase tracking-wider hover:bg-primary/90 transition-colors disabled:opacity-50 flex items-center justify-center gap-2" className="w-full bg-primary text-background py-3 font-bold uppercase tracking-wider hover:bg-primary/90 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
data-testid="button-login" data-testid="button-submit"
> >
{isLoading ? ( {isLoading ? (
<>Processing...</> <>Processing...</>
) : ( ) : mode === 'login' ? (
<> <>
<Lock className="w-4 h-4" /> <Lock className="w-4 h-4" />
Authenticate Authenticate
</> </>
) : (
<>
<UserPlus className="w-4 h-4" />
Create Account
</>
)} )}
</button> </button>
</form> </form>

View file

@ -1,9 +1,8 @@
import type { Express, Request, Response, NextFunction } from "express"; import type { Express, Request, Response, NextFunction } from "express";
import { createServer, type Server } from "http"; import { createServer, type Server } from "http";
import { storage } from "./storage"; import { storage } from "./storage";
import { loginSchema } from "@shared/schema"; import { loginSchema, signupSchema } from "@shared/schema";
import bcrypt from "bcrypt"; import { supabase } from "./supabase";
import crypto from "crypto";
import { getChatResponse } from "./openai"; import { getChatResponse } from "./openai";
// Extend session type // Extend session type
@ -11,7 +10,7 @@ declare module 'express-session' {
interface SessionData { interface SessionData {
userId?: string; userId?: string;
isAdmin?: boolean; isAdmin?: boolean;
token?: string; accessToken?: string;
} }
} }
@ -34,69 +33,48 @@ function requireAdmin(req: Request, res: Response, next: NextFunction) {
next(); next();
} }
// Generate JWT-like token
function generateToken(userId: string, username: string): string {
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString('base64url');
const payload = Buffer.from(JSON.stringify({
userId,
username,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600 // 1 hour
})).toString('base64url');
const signature = crypto.createHmac('sha256', process.env.SESSION_SECRET || 'dev-secret')
.update(`${header}.${payload}`)
.digest('base64url');
return `${header}.${payload}.${signature}`;
}
export async function registerRoutes( export async function registerRoutes(
httpServer: Server, httpServer: Server,
app: Express app: Express
): Promise<Server> { ): Promise<Server> {
// ========== AUTH ROUTES ========== // ========== AUTH ROUTES (Supabase Auth) ==========
// Login - creates JWT token and stores in sessions table // Login via Supabase Auth
app.post("/api/auth/login", async (req, res) => { app.post("/api/auth/login", async (req, res) => {
try { try {
const result = loginSchema.safeParse(req.body); const result = loginSchema.safeParse(req.body);
if (!result.success) { if (!result.success) {
return res.status(400).json({ error: "Invalid credentials" }); return res.status(400).json({ error: "Invalid email or password format" });
} }
const { username, password } = result.data; const { email, password } = result.data;
const user = await storage.getUserByUsername(username);
if (!user) { // Authenticate with Supabase
return res.status(401).json({ error: "Invalid credentials" }); const { data, error } = await supabase.auth.signInWithPassword({
} email,
password,
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Generate token like your other apps
const token = generateToken(user.id, user.username);
const expiresAt = new Date(Date.now() + 3600 * 1000); // 1 hour
// Store session in sessions table (like your other apps)
await storage.createSession({
user_id: user.id,
username: user.username,
token,
expires_at: expiresAt.toISOString()
}); });
// Also set express session for this app if (error || !data.user) {
return res.status(401).json({ error: error?.message || "Invalid credentials" });
}
// Get user profile from public.profiles
const profile = await storage.getProfile(data.user.id);
// Check if user is admin (based on profile role or email)
const isAdmin = profile?.role === 'admin' || email.includes('admin');
// Set express session
req.session.regenerate((err) => { req.session.regenerate((err) => {
if (err) { if (err) {
return res.status(500).json({ error: "Session error" }); return res.status(500).json({ error: "Session error" });
} }
req.session.userId = user.id; req.session.userId = data.user.id;
req.session.isAdmin = user.is_admin ?? false; req.session.isAdmin = isAdmin;
req.session.token = token; req.session.accessToken = data.session?.access_token;
req.session.save((saveErr) => { req.session.save((saveErr) => {
if (saveErr) { if (saveErr) {
@ -105,11 +83,11 @@ export async function registerRoutes(
res.json({ res.json({
success: true, success: true,
token,
user: { user: {
id: user.id, id: data.user.id,
username: user.username, email: data.user.email,
isAdmin: user.is_admin username: profile?.username || data.user.email?.split('@')[0],
isAdmin
} }
}); });
}); });
@ -120,15 +98,59 @@ export async function registerRoutes(
} }
}); });
// Logout // Signup via Supabase Auth
app.post("/api/auth/logout", (req, res) => { app.post("/api/auth/signup", async (req, res) => {
req.session.destroy((err) => { try {
if (err) { const result = signupSchema.safeParse(req.body);
return res.status(500).json({ error: "Logout failed" }); if (!result.success) {
return res.status(400).json({ error: result.error.errors[0].message });
} }
res.clearCookie('connect.sid');
res.json({ success: true }); const { email, password, username } = result.data;
});
// Create user in Supabase Auth
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: { username: username || email.split('@')[0] }
}
});
if (error || !data.user) {
return res.status(400).json({ error: error?.message || "Signup failed" });
}
res.json({
success: true,
message: data.session ? "Account created successfully" : "Please check your email to confirm your account",
user: {
id: data.user.id,
email: data.user.email
}
});
} catch (err: any) {
console.error('Signup error:', err);
res.status(500).json({ error: err.message });
}
});
// Logout
app.post("/api/auth/logout", async (req, res) => {
try {
// Sign out from Supabase
await supabase.auth.signOut();
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: "Logout failed" });
}
res.clearCookie('connect.sid');
res.json({ success: true });
});
} catch (err: any) {
res.status(500).json({ error: err.message });
}
}); });
// Get current session // Get current session
@ -137,17 +159,16 @@ export async function registerRoutes(
return res.json({ authenticated: false }); return res.json({ authenticated: false });
} }
const user = await storage.getUser(req.session.userId); // Get profile from storage
if (!user) { const profile = await storage.getProfile(req.session.userId);
return res.json({ authenticated: false });
}
res.json({ res.json({
authenticated: true, authenticated: true,
user: { user: {
id: user.id, id: req.session.userId,
username: user.username, username: profile?.username || 'User',
isAdmin: user.is_admin email: profile?.email,
isAdmin: req.session.isAdmin
} }
}); });
}); });

View file

@ -1,14 +1,7 @@
import { type User, type Profile, type Project } from "@shared/schema"; import { type Profile, type Project } from "@shared/schema";
import { supabase } from "./supabase"; import { supabase } from "./supabase";
export interface IStorage { export interface IStorage {
// Users
getUser(id: string): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
// Sessions
createSession(session: { user_id: string; username: string; token: string; expires_at: string }): Promise<any>;
// Profiles // Profiles
getProfiles(): Promise<Profile[]>; getProfiles(): Promise<Profile[]>;
getProfile(id: string): Promise<Profile | undefined>; getProfile(id: string): Promise<Profile | undefined>;
@ -49,39 +42,6 @@ export interface IStorage {
export class SupabaseStorage implements IStorage { export class SupabaseStorage implements IStorage {
async getUser(id: string): Promise<User | undefined> {
const { data, error } = await supabase
.from('users')
.select('*')
.eq('id', id)
.single();
if (error || !data) return undefined;
return data as User;
}
async getUserByUsername(username: string): Promise<User | undefined> {
const { data, error } = await supabase
.from('users')
.select('*')
.eq('username', username)
.single();
if (error || !data) return undefined;
return data as User;
}
async createSession(session: { user_id: string; username: string; token: string; expires_at: string }): Promise<any> {
const { data, error } = await supabase
.from('sessions')
.insert(session)
.select()
.single();
if (error) throw error;
return data;
}
async getProfiles(): Promise<Profile[]> { async getProfiles(): Promise<Profile[]> {
const { data, error } = await supabase const { data, error } = await supabase
.from('profiles') .from('profiles')

View file

@ -2,27 +2,9 @@ import { pgTable, text, varchar, boolean, integer, timestamp, json } from "drizz
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { z } from "zod"; import { z } from "zod";
// Users table (auth) // Profiles table (linked to Supabase auth.users via id)
export const users = pgTable("users", {
id: varchar("id").primaryKey(),
username: text("username").notNull().unique(),
password: text("password").notNull(),
is_active: boolean("is_active").default(true),
is_admin: boolean("is_admin").default(false),
created_at: timestamp("created_at").defaultNow(),
});
export const insertUserSchema = createInsertSchema(users).pick({
username: true,
password: true,
});
export type InsertUser = z.infer<typeof insertUserSchema>;
export type User = typeof users.$inferSelect;
// Profiles table (rich user data)
export const profiles = pgTable("profiles", { export const profiles = pgTable("profiles", {
id: varchar("id").primaryKey(), id: varchar("id").primaryKey(), // References auth.users(id)
username: text("username"), username: text("username"),
role: text("role").default("member"), role: text("role").default("member"),
onboarded: boolean("onboarded").default(false), onboarded: boolean("onboarded").default(false),
@ -47,7 +29,6 @@ export const profiles = pgTable("profiles", {
}); });
export const insertProfileSchema = createInsertSchema(profiles).omit({ export const insertProfileSchema = createInsertSchema(profiles).omit({
id: true,
created_at: true, created_at: true,
updated_at: true, updated_at: true,
}); });
@ -82,10 +63,19 @@ export const insertProjectSchema = createInsertSchema(projects).omit({
export type InsertProject = z.infer<typeof insertProjectSchema>; export type InsertProject = z.infer<typeof insertProjectSchema>;
export type Project = typeof projects.$inferSelect; export type Project = typeof projects.$inferSelect;
// Login schema for validation // Login schema for Supabase Auth (email + password)
export const loginSchema = z.object({ export const loginSchema = z.object({
username: z.string().min(1, "Username is required"), email: z.string().email("Valid email is required"),
password: z.string().min(1, "Password is required"), password: z.string().min(6, "Password must be at least 6 characters"),
}); });
export type LoginInput = z.infer<typeof loginSchema>; export type LoginInput = z.infer<typeof loginSchema>;
// Signup schema
export const signupSchema = z.object({
email: z.string().email("Valid email is required"),
password: z.string().min(6, "Password must be at least 6 characters"),
username: z.string().min(2, "Username must be at least 2 characters").optional(),
});
export type SignupInput = z.infer<typeof signupSchema>;