mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:07:20 +00:00
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:
parent
091461a865
commit
0cfb38d847
7 changed files with 215 additions and 159 deletions
6
.replit
6
.replit
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
13
client/src/lib/supabase.ts
Normal file
13
client/src/lib/supabase.ts
Normal 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 || ''
|
||||
);
|
||||
|
|
@ -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);
|
||||
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>
|
||||
|
|
|
|||
139
server/routes.ts
139
server/routes.ts
|
|
@ -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) {
|
||||
|
|
@ -105,11 +83,11 @@ export async function registerRoutes(
|
|||
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
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,8 +98,49 @@ export async function registerRoutes(
|
|||
}
|
||||
});
|
||||
|
||||
// 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 });
|
||||
}
|
||||
|
||||
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", (req, res) => {
|
||||
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" });
|
||||
|
|
@ -129,6 +148,9 @@ export async function registerRoutes(
|
|||
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
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue