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]
mockupState = "FULLSTACK"
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";
interface User {
id: string;
email?: string;
username: string;
isAdmin: boolean;
}
@ -12,7 +13,8 @@ interface AuthContextType {
isLoading: boolean;
isAuthenticated: 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>;
}
@ -21,26 +23,26 @@ const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient();
const { data: session, isLoading, isFetching } = useQuery({
const { data: session, isLoading } = useQuery({
queryKey: ["session"],
queryFn: async () => {
const res = await fetch("/api/auth/session", { credentials: "include" });
return res.json();
},
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
});
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", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ username, password }),
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
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({
mutationFn: async () => {
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) => {
await loginMutation.mutateAsync({ username, password });
const login = async (email: string, password: string) => {
await loginMutation.mutateAsync({ email, password });
};
const signup = async (email: string, password: string, username?: string) => {
return await signupMutation.mutateAsync({ email, password, username });
};
const logout = async () => {
@ -76,6 +98,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
isAuthenticated: !!session?.authenticated,
isAdmin: session?.user?.isAdmin || false,
login,
signup,
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 { motion } from "framer-motion";
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 gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png';
export default function Login() {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { login } = useAuth();
const [mode, setMode] = useState<'login' | 'signup'>('login');
const { login, signup } = useAuth();
const [, setLocation] = useLocation();
const handleSubmit = async (e: React.FormEvent) => {
@ -18,14 +20,22 @@ export default function Login() {
if (isLoading) return;
setError("");
setSuccess("");
setIsLoading(true);
try {
await login(username, password);
await new Promise(resolve => setTimeout(resolve, 100));
setLocation("/admin");
if (mode === 'login') {
await login(email, password);
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) {
setError(err.message || "Login failed");
setError(err.message || `${mode === 'login' ? 'Login' : 'Signup'} failed`);
} finally {
setIsLoading(false);
}
};
@ -50,10 +60,31 @@ export default function Login() {
AeThex Command
</h1>
<p className="text-muted-foreground text-sm mt-2">
Authorized Personnel Only
{mode === 'login' ? 'Authorized Personnel Only' : 'Create Your Account'}
</p>
</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">
{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">
@ -62,17 +93,23 @@ export default function Login() {
</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">
<label className="text-xs text-muted-foreground uppercase tracking-widest">
Username
Email
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
type="email"
value={email}
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"
placeholder="Enter username"
data-testid="input-username"
placeholder="Enter email"
data-testid="input-email"
required
/>
</div>
@ -86,9 +123,10 @@ export default function Login() {
value={password}
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"
placeholder="Enter password"
placeholder={mode === 'signup' ? 'Min 6 characters' : 'Enter password'}
data-testid="input-password"
required
minLength={mode === 'signup' ? 6 : undefined}
/>
</div>
@ -96,15 +134,20 @@ export default function Login() {
type="submit"
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"
data-testid="button-login"
data-testid="button-submit"
>
{isLoading ? (
<>Processing...</>
) : (
) : mode === 'login' ? (
<>
<Lock className="w-4 h-4" />
Authenticate
</>
) : (
<>
<UserPlus className="w-4 h-4" />
Create Account
</>
)}
</button>
</form>

View file

@ -1,9 +1,8 @@
import type { Express, Request, Response, NextFunction } from "express";
import { createServer, type Server } from "http";
import { storage } from "./storage";
import { loginSchema } from "@shared/schema";
import bcrypt from "bcrypt";
import crypto from "crypto";
import { loginSchema, signupSchema } from "@shared/schema";
import { supabase } from "./supabase";
import { getChatResponse } from "./openai";
// Extend session type
@ -11,7 +10,7 @@ declare module 'express-session' {
interface SessionData {
userId?: string;
isAdmin?: boolean;
token?: string;
accessToken?: string;
}
}
@ -34,69 +33,48 @@ function requireAdmin(req: Request, res: Response, next: NextFunction) {
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(
httpServer: Server,
app: Express
): 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) => {
try {
const result = loginSchema.safeParse(req.body);
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 user = await storage.getUserByUsername(username);
const { email, password } = result.data;
if (!user) {
return res.status(401).json({ error: "Invalid credentials" });
}
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()
// Authenticate with Supabase
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
// 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) => {
if (err) {
return res.status(500).json({ error: "Session error" });
}
req.session.userId = user.id;
req.session.isAdmin = user.is_admin ?? false;
req.session.token = token;
req.session.userId = data.user.id;
req.session.isAdmin = isAdmin;
req.session.accessToken = data.session?.access_token;
req.session.save((saveErr) => {
if (saveErr) {
@ -104,13 +82,13 @@ export async function registerRoutes(
}
res.json({
success: true,
token,
success: true,
user: {
id: user.id,
username: user.username,
isAdmin: user.is_admin
}
id: data.user.id,
email: data.user.email,
username: profile?.username || data.user.email?.split('@')[0],
isAdmin
}
});
});
});
@ -120,15 +98,59 @@ export async function registerRoutes(
}
});
// Logout
app.post("/api/auth/logout", (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: "Logout failed" });
// Signup via Supabase Auth
app.post("/api/auth/signup", async (req, res) => {
try {
const result = signupSchema.safeParse(req.body);
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
@ -137,17 +159,16 @@ export async function registerRoutes(
return res.json({ authenticated: false });
}
const user = await storage.getUser(req.session.userId);
if (!user) {
return res.json({ authenticated: false });
}
// Get profile from storage
const profile = await storage.getProfile(req.session.userId);
res.json({
authenticated: true,
user: {
id: user.id,
username: user.username,
isAdmin: user.is_admin
id: req.session.userId,
username: profile?.username || 'User',
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";
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
getProfiles(): Promise<Profile[]>;
getProfile(id: string): Promise<Profile | undefined>;
@ -49,39 +42,6 @@ export interface 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[]> {
const { data, error } = await supabase
.from('profiles')

View file

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