mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-14 15:36:43 +03:00
Added Better Auth
This commit is contained in:
@@ -4,6 +4,7 @@ import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
|
||||
import { toast, Toaster } from 'sonner';
|
||||
import { showErrorToast } from '@/lib/utils';
|
||||
@@ -11,43 +12,29 @@ import { showErrorToast } from '@/lib/utils';
|
||||
|
||||
export function LoginForm() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { login } = useAuth();
|
||||
|
||||
async function handleLogin(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
const form = e.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
const username = formData.get('username') as string | null;
|
||||
const email = formData.get('email') as string | null;
|
||||
const password = formData.get('password') as string | null;
|
||||
|
||||
if (!username || !password) {
|
||||
toast.error('Please enter both username and password');
|
||||
if (!email || !password) {
|
||||
toast.error('Please enter both email and password');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const loginData = { username, password };
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(loginData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Login successful!');
|
||||
// Small delay before redirecting to see the success message
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} else {
|
||||
showErrorToast(data.error || 'Login failed. Please try again.', toast);
|
||||
}
|
||||
await login(email, password);
|
||||
toast.success('Login successful!');
|
||||
// Small delay before redirecting to see the success message
|
||||
setTimeout(() => {
|
||||
window.location.href = '/dashboard';
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
} finally {
|
||||
@@ -80,16 +67,16 @@ export function LoginForm() {
|
||||
<form id="login-form" onSubmit={handleLogin}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-medium mb-1">
|
||||
Username
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-1">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
placeholder="Enter your username"
|
||||
placeholder="Enter your email"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,11 @@ import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { toast, Toaster } from 'sonner';
|
||||
import { showErrorToast } from '@/lib/utils';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
|
||||
export function SignupForm() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { register } = useAuth();
|
||||
|
||||
async function handleSignup(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
@@ -31,28 +33,13 @@ export function SignupForm() {
|
||||
return;
|
||||
}
|
||||
|
||||
const signupData = { username, email, password };
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(signupData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Account created successfully! Redirecting to dashboard...');
|
||||
// Small delay before redirecting to see the success message
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1500);
|
||||
} else {
|
||||
showErrorToast(data.error || 'Failed to create account. Please try again.', toast);
|
||||
}
|
||||
await register(username, email, password);
|
||||
toast.success('Account created successfully! Redirecting to dashboard...');
|
||||
// Small delay before redirecting to see the success message
|
||||
setTimeout(() => {
|
||||
window.location.href = '/dashboard';
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
} finally {
|
||||
|
||||
147
src/hooks/useAuth-legacy.ts
Normal file
147
src/hooks/useAuth-legacy.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
createContext,
|
||||
useContext,
|
||||
type Context,
|
||||
} from "react";
|
||||
import { authApi } from "@/lib/api";
|
||||
import type { ExtendedUser } from "@/types/user";
|
||||
|
||||
interface AuthContextType {
|
||||
user: ExtendedUser | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
register: (
|
||||
username: string,
|
||||
email: string,
|
||||
password: string
|
||||
) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
refreshUser: () => Promise<void>; // Added refreshUser function
|
||||
}
|
||||
|
||||
const AuthContext: Context<AuthContextType | undefined> = createContext<
|
||||
AuthContextType | undefined
|
||||
>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<ExtendedUser | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Function to refetch the user data
|
||||
const refreshUser = async () => {
|
||||
// not using loading state to keep the ui seamless and refresh the data in bg
|
||||
// setIsLoading(true);
|
||||
try {
|
||||
const user = await authApi.getCurrentUser();
|
||||
setUser(user);
|
||||
} catch (err: any) {
|
||||
setUser(null);
|
||||
console.error("Failed to refresh user data", err);
|
||||
} finally {
|
||||
// setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Automatically check the user status when the app loads
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const user = await authApi.getCurrentUser();
|
||||
|
||||
console.log("User data fetched:", user);
|
||||
|
||||
setUser(user);
|
||||
} catch (err: any) {
|
||||
setUser(null);
|
||||
|
||||
// Redirect user based on error
|
||||
if (err?.message === "No users found") {
|
||||
window.location.href = "/signup";
|
||||
} else {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
console.error("Auth check failed", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const user = await authApi.login(username, password);
|
||||
setUser(user);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Login failed");
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (
|
||||
username: string,
|
||||
email: string,
|
||||
password: string
|
||||
) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const user = await authApi.register(username, email, password);
|
||||
setUser(user);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Registration failed");
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await authApi.logout();
|
||||
setUser(null);
|
||||
window.location.href = "/login";
|
||||
} catch (err) {
|
||||
console.error("Logout error:", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Create the context value with the added refreshUser function
|
||||
const contextValue = {
|
||||
user,
|
||||
isLoading,
|
||||
error,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
refreshUser,
|
||||
};
|
||||
|
||||
// Return the provider with the context value
|
||||
return React.createElement(
|
||||
AuthContext.Provider,
|
||||
{ value: contextValue },
|
||||
children
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -6,21 +6,22 @@ import {
|
||||
useContext,
|
||||
type Context,
|
||||
} from "react";
|
||||
import { authApi } from "@/lib/api";
|
||||
import type { ExtendedUser } from "@/types/user";
|
||||
import { authClient, useSession as useBetterAuthSession } from "@/lib/auth-client";
|
||||
import type { Session, AuthUser } from "@/lib/auth-client";
|
||||
|
||||
interface AuthContextType {
|
||||
user: ExtendedUser | null;
|
||||
user: AuthUser | null;
|
||||
session: Session | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
login: (email: string, password: string, username?: string) => Promise<void>;
|
||||
register: (
|
||||
username: string,
|
||||
email: string,
|
||||
password: string
|
||||
) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
refreshUser: () => Promise<void>; // Added refreshUser function
|
||||
refreshUser: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext: Context<AuthContextType | undefined> = createContext<
|
||||
@@ -28,60 +29,53 @@ const AuthContext: Context<AuthContextType | undefined> = createContext<
|
||||
>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<ExtendedUser | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const betterAuthSession = useBetterAuthSession();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Function to refetch the user data
|
||||
const refreshUser = async () => {
|
||||
// not using loading state to keep the ui seamless and refresh the data in bg
|
||||
// setIsLoading(true);
|
||||
try {
|
||||
const user = await authApi.getCurrentUser();
|
||||
setUser(user);
|
||||
} catch (err: any) {
|
||||
setUser(null);
|
||||
console.error("Failed to refresh user data", err);
|
||||
} finally {
|
||||
// setIsLoading(false);
|
||||
}
|
||||
};
|
||||
// Derive user and session from Better Auth hook
|
||||
const user = betterAuthSession.data?.user || null;
|
||||
const session = betterAuthSession.data || null;
|
||||
|
||||
// Automatically check the user status when the app loads
|
||||
// Check if this is first load and redirect if needed
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const user = await authApi.getCurrentUser();
|
||||
|
||||
console.log("User data fetched:", user);
|
||||
|
||||
setUser(user);
|
||||
} catch (err: any) {
|
||||
setUser(null);
|
||||
|
||||
// Redirect user based on error
|
||||
if (err?.message === "No users found") {
|
||||
window.location.href = "/signup";
|
||||
} else {
|
||||
window.location.href = "/login";
|
||||
const checkFirstUser = async () => {
|
||||
if (!betterAuthSession.isPending && !user) {
|
||||
try {
|
||||
// Check if there are any users in the system
|
||||
const response = await fetch("/api/auth/check-users");
|
||||
if (response.status === 404) {
|
||||
// No users found, redirect to signup
|
||||
window.location.href = "/signup";
|
||||
} else if (!window.location.pathname.includes("/login")) {
|
||||
// User not authenticated, redirect to login
|
||||
window.location.href = "/login";
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to check users:", err);
|
||||
}
|
||||
console.error("Auth check failed", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, []);
|
||||
checkFirstUser();
|
||||
}, [betterAuthSession.isPending, user]);
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
const login = async (email: string, password: string) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const user = await authApi.login(username, password);
|
||||
setUser(user);
|
||||
const result = await authClient.signIn.email({
|
||||
email,
|
||||
password,
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message || "Login failed");
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Login failed");
|
||||
const message = err instanceof Error ? err.message : "Login failed";
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -96,10 +90,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const user = await authApi.register(username, email, password);
|
||||
setUser(user);
|
||||
const result = await authClient.signUp.email({
|
||||
email,
|
||||
password,
|
||||
name: username, // Better Auth uses 'name' field
|
||||
username, // Also pass username as additional field
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message || "Registration failed");
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Registration failed");
|
||||
const message = err instanceof Error ? err.message : "Registration failed";
|
||||
setError(message);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -109,9 +113,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const logout = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await authApi.logout();
|
||||
setUser(null);
|
||||
window.location.href = "/login";
|
||||
await authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
window.location.href = "/login";
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Logout error:", err);
|
||||
} finally {
|
||||
@@ -119,10 +127,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Create the context value with the added refreshUser function
|
||||
const refreshUser = async () => {
|
||||
// Better Auth automatically handles session refresh
|
||||
// We can force a refetch if needed
|
||||
await betterAuthSession.refetch();
|
||||
};
|
||||
|
||||
// Create the context value
|
||||
const contextValue = {
|
||||
user,
|
||||
isLoading,
|
||||
user: user as AuthUser | null,
|
||||
session,
|
||||
isLoading: isLoading || betterAuthSession.isPending,
|
||||
error,
|
||||
login,
|
||||
register,
|
||||
@@ -145,3 +160,6 @@ export function useAuth() {
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// Export the Better Auth session hook for direct use when needed
|
||||
export { useBetterAuthSession };
|
||||
22
src/lib/auth-client.ts
Normal file
22
src/lib/auth-client.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
// The base URL is optional when running on the same domain
|
||||
// Better Auth will use the current domain by default
|
||||
});
|
||||
|
||||
// Export commonly used methods for convenience
|
||||
export const {
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
useSession,
|
||||
sendVerificationEmail,
|
||||
resetPassword,
|
||||
requestPasswordReset,
|
||||
getSession
|
||||
} = authClient;
|
||||
|
||||
// Export types
|
||||
export type Session = Awaited<ReturnType<typeof authClient.getSession>>["data"];
|
||||
export type AuthUser = Session extends { user: infer U } ? U : never;
|
||||
76
src/lib/auth-config.ts
Normal file
76
src/lib/auth-config.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { sso, oidcProvider } from "better-auth/plugins";
|
||||
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
|
||||
|
||||
// Generate or use existing JWT secret
|
||||
const JWT_SECRET = process.env.JWT_SECRET || process.env.BETTER_AUTH_SECRET;
|
||||
|
||||
if (!JWT_SECRET) {
|
||||
throw new Error("JWT_SECRET or BETTER_AUTH_SECRET environment variable is required");
|
||||
}
|
||||
|
||||
// This function will be called with the actual database instance
|
||||
export function createAuth(db: BunSQLiteDatabase) {
|
||||
return betterAuth({
|
||||
// Database configuration
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "sqlite",
|
||||
usePlural: true, // Our tables use plural names (users, not user)
|
||||
}),
|
||||
|
||||
// Base URL configuration
|
||||
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||
|
||||
// Authentication methods
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: false, // We'll enable this later
|
||||
sendResetPassword: async ({ user, url, token }, request) => {
|
||||
// TODO: Implement email sending for password reset
|
||||
console.log("Password reset requested for:", user.email);
|
||||
console.log("Reset URL:", url);
|
||||
},
|
||||
},
|
||||
|
||||
// Session configuration
|
||||
session: {
|
||||
cookieName: "better-auth-session",
|
||||
updateSessionCookieAge: true,
|
||||
expiresIn: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
|
||||
// User configuration
|
||||
user: {
|
||||
additionalFields: {
|
||||
// We can add custom fields here if needed
|
||||
},
|
||||
},
|
||||
|
||||
// Plugins for future OIDC/SSO support
|
||||
plugins: [
|
||||
// SSO plugin for OIDC client support
|
||||
sso({
|
||||
provisionUser: async (data) => {
|
||||
// Custom user provisioning logic for SSO users
|
||||
console.log("Provisioning SSO user:", data);
|
||||
return data;
|
||||
},
|
||||
}),
|
||||
|
||||
// OIDC Provider plugin (for future use when we want to be an OIDC provider)
|
||||
oidcProvider({
|
||||
loginPage: "/signin",
|
||||
consentPage: "/oauth/consent",
|
||||
metadata: {
|
||||
issuer: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
// Trusted origins for CORS
|
||||
trustedOrigins: [
|
||||
process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||
],
|
||||
});
|
||||
}
|
||||
179
src/lib/auth-oidc-config.example.ts
Normal file
179
src/lib/auth-oidc-config.example.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Example OIDC/SSO Configuration for Better Auth
|
||||
*
|
||||
* This file demonstrates how to enable OIDC and SSO features in Gitea Mirror.
|
||||
* To use: Copy this file to auth-oidc-config.ts and update the auth.ts import.
|
||||
*/
|
||||
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { sso } from "better-auth/plugins/sso";
|
||||
import { oidcProvider } from "better-auth/plugins/oidc";
|
||||
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
|
||||
|
||||
export function createAuthWithOIDC(db: BunSQLiteDatabase) {
|
||||
return betterAuth({
|
||||
// Database configuration
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "sqlite",
|
||||
usePlural: true,
|
||||
}),
|
||||
|
||||
// Base configuration
|
||||
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||
basePath: "/api/auth",
|
||||
|
||||
// Email/Password authentication
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: false,
|
||||
},
|
||||
|
||||
// Session configuration
|
||||
session: {
|
||||
cookieName: "better-auth-session",
|
||||
updateSessionCookieAge: true,
|
||||
expiresIn: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
|
||||
// User configuration with additional fields
|
||||
user: {
|
||||
additionalFields: {
|
||||
username: {
|
||||
type: "string",
|
||||
required: true,
|
||||
defaultValue: "user",
|
||||
input: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// OAuth2 providers (examples)
|
||||
socialProviders: {
|
||||
github: {
|
||||
enabled: !!process.env.GITHUB_OAUTH_CLIENT_ID,
|
||||
clientId: process.env.GITHUB_OAUTH_CLIENT_ID!,
|
||||
clientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET!,
|
||||
},
|
||||
google: {
|
||||
enabled: !!process.env.GOOGLE_OAUTH_CLIENT_ID,
|
||||
clientId: process.env.GOOGLE_OAUTH_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET!,
|
||||
},
|
||||
},
|
||||
|
||||
// Plugins
|
||||
plugins: [
|
||||
// SSO Plugin - For OIDC/SAML client functionality
|
||||
sso({
|
||||
// Auto-provision users from SSO providers
|
||||
provisionUser: async (data) => {
|
||||
console.log("Provisioning SSO user:", data.email);
|
||||
|
||||
// Custom logic to set username from email
|
||||
const username = data.email.split('@')[0];
|
||||
|
||||
return {
|
||||
...data,
|
||||
username,
|
||||
};
|
||||
},
|
||||
|
||||
// Organization provisioning for enterprise SSO
|
||||
organizationProvisioning: {
|
||||
disabled: false,
|
||||
defaultRole: "member",
|
||||
getRole: async (user) => {
|
||||
// Custom logic to determine user role
|
||||
// For admin emails, grant admin role
|
||||
if (user.email?.endsWith('@admin.example.com')) {
|
||||
return 'admin';
|
||||
}
|
||||
return 'member';
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
// OIDC Provider Plugin - Makes Gitea Mirror an OIDC provider
|
||||
oidcProvider({
|
||||
// Login page for OIDC authentication flow
|
||||
loginPage: "/login",
|
||||
|
||||
// Consent page for OAuth2 authorization
|
||||
consentPage: "/oauth/consent",
|
||||
|
||||
// Allow dynamic client registration
|
||||
allowDynamicClientRegistration: false,
|
||||
|
||||
// OIDC metadata configuration
|
||||
metadata: {
|
||||
issuer: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||
authorization_endpoint: "/api/auth/oauth2/authorize",
|
||||
token_endpoint: "/api/auth/oauth2/token",
|
||||
userinfo_endpoint: "/api/auth/oauth2/userinfo",
|
||||
jwks_uri: "/api/auth/jwks",
|
||||
},
|
||||
|
||||
// Additional user info claims
|
||||
getAdditionalUserInfoClaim: (user, scopes) => {
|
||||
const claims: Record<string, any> = {};
|
||||
|
||||
// Add custom claims based on scopes
|
||||
if (scopes.includes('profile')) {
|
||||
claims.username = user.username;
|
||||
claims.preferred_username = user.username;
|
||||
}
|
||||
|
||||
if (scopes.includes('gitea')) {
|
||||
// Add Gitea-specific claims
|
||||
claims.gitea_admin = false; // Customize based on your logic
|
||||
claims.gitea_repos = []; // Could fetch user's repositories
|
||||
}
|
||||
|
||||
return claims;
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
// Trusted origins for CORS
|
||||
trustedOrigins: [
|
||||
process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||
// Add your OIDC client domains here
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Environment variables needed:
|
||||
/*
|
||||
# OAuth2 Providers (optional)
|
||||
GITHUB_OAUTH_CLIENT_ID=your-github-client-id
|
||||
GITHUB_OAUTH_CLIENT_SECRET=your-github-client-secret
|
||||
GOOGLE_OAUTH_CLIENT_ID=your-google-client-id
|
||||
GOOGLE_OAUTH_CLIENT_SECRET=your-google-client-secret
|
||||
|
||||
# SSO Configuration (when registering providers)
|
||||
SSO_PROVIDER_ISSUER=https://idp.example.com
|
||||
SSO_PROVIDER_CLIENT_ID=your-client-id
|
||||
SSO_PROVIDER_CLIENT_SECRET=your-client-secret
|
||||
*/
|
||||
|
||||
// Example: Registering an SSO provider programmatically
|
||||
/*
|
||||
import { authClient } from "./auth-client";
|
||||
|
||||
// Register corporate SSO
|
||||
await authClient.sso.register({
|
||||
issuer: "https://login.microsoftonline.com/tenant-id/v2.0",
|
||||
domain: "company.com",
|
||||
clientId: process.env.AZURE_CLIENT_ID!,
|
||||
clientSecret: process.env.AZURE_CLIENT_SECRET!,
|
||||
providerId: "azure-ad",
|
||||
mapping: {
|
||||
id: "sub",
|
||||
email: "email",
|
||||
emailVerified: "email_verified",
|
||||
name: "name",
|
||||
image: "picture",
|
||||
},
|
||||
});
|
||||
*/
|
||||
64
src/lib/auth.ts
Normal file
64
src/lib/auth.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { db } from "./db";
|
||||
|
||||
// Generate or use existing JWT secret
|
||||
const JWT_SECRET = process.env.JWT_SECRET || process.env.BETTER_AUTH_SECRET;
|
||||
|
||||
if (!JWT_SECRET) {
|
||||
throw new Error("JWT_SECRET or BETTER_AUTH_SECRET environment variable is required");
|
||||
}
|
||||
|
||||
export const auth = betterAuth({
|
||||
// Database configuration
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "sqlite",
|
||||
usePlural: true, // Our tables use plural names (users, not user)
|
||||
}),
|
||||
|
||||
// Base URL configuration
|
||||
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||
basePath: "/api/auth", // Specify the base path for auth endpoints
|
||||
|
||||
// Authentication methods
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: false, // We'll enable this later
|
||||
sendResetPassword: async ({ user, url, token }, request) => {
|
||||
// TODO: Implement email sending for password reset
|
||||
console.log("Password reset requested for:", user.email);
|
||||
console.log("Reset URL:", url);
|
||||
},
|
||||
},
|
||||
|
||||
// Session configuration
|
||||
session: {
|
||||
cookieName: "better-auth-session",
|
||||
updateSessionCookieAge: true,
|
||||
expiresIn: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
|
||||
// User configuration
|
||||
user: {
|
||||
additionalFields: {
|
||||
// Keep the username field from our existing schema
|
||||
username: {
|
||||
type: "string",
|
||||
required: true,
|
||||
defaultValue: "user", // Default for migration
|
||||
input: true, // Allow in signup form
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Add plugins for SSO and OIDC support in the future
|
||||
// plugins: [],
|
||||
|
||||
// Trusted origins for CORS
|
||||
trustedOrigins: [
|
||||
process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||
],
|
||||
});
|
||||
|
||||
// Export type for use in other parts of the app
|
||||
export type Auth = typeof auth;
|
||||
@@ -23,14 +23,14 @@ let sqlite: Database;
|
||||
try {
|
||||
sqlite = new Database(dbPath);
|
||||
console.log("Successfully connected to SQLite database using Bun's native driver");
|
||||
|
||||
// Run Drizzle migrations if needed
|
||||
runDrizzleMigrations();
|
||||
} catch (error) {
|
||||
console.error("Error opening database:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Create drizzle instance with the SQLite client
|
||||
export const db = drizzle({ client: sqlite });
|
||||
|
||||
/**
|
||||
* Run Drizzle migrations
|
||||
*/
|
||||
@@ -57,8 +57,18 @@ function runDrizzleMigrations() {
|
||||
}
|
||||
}
|
||||
|
||||
// Create drizzle instance with the SQLite client
|
||||
export const db = drizzle({ client: sqlite });
|
||||
// Run Drizzle migrations after db is initialized
|
||||
runDrizzleMigrations();
|
||||
|
||||
// Export all table definitions from schema
|
||||
export { users, events, configs, repositories, mirrorJobs, organizations } from "./schema";
|
||||
export {
|
||||
users,
|
||||
events,
|
||||
configs,
|
||||
repositories,
|
||||
mirrorJobs,
|
||||
organizations,
|
||||
sessions,
|
||||
accounts,
|
||||
verificationTokens
|
||||
} from "./schema";
|
||||
|
||||
@@ -8,6 +8,7 @@ export const userSchema = z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
email: z.string().email(),
|
||||
emailVerified: z.boolean().default(false),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
@@ -215,6 +216,7 @@ export const users = sqliteTable("users", {
|
||||
username: text("username").notNull(),
|
||||
password: text("password").notNull(),
|
||||
email: text("email").notNull(),
|
||||
emailVerified: integer("email_verified", { mode: "boolean" }).notNull().default(false),
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
@@ -434,6 +436,70 @@ export const organizations = sqliteTable("organizations", {
|
||||
};
|
||||
});
|
||||
|
||||
// ===== Better Auth Tables =====
|
||||
|
||||
// Sessions table
|
||||
export const sessions = sqliteTable("sessions", {
|
||||
id: text("id").primaryKey(),
|
||||
token: text("token").notNull().unique(),
|
||||
userId: text("user_id").notNull().references(() => users.id),
|
||||
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
}, (table) => {
|
||||
return {
|
||||
userIdIdx: index("idx_sessions_user_id").on(table.userId),
|
||||
tokenIdx: index("idx_sessions_token").on(table.token),
|
||||
expiresAtIdx: index("idx_sessions_expires_at").on(table.expiresAt),
|
||||
};
|
||||
});
|
||||
|
||||
// Accounts table (for OAuth providers and credentials)
|
||||
export const accounts = sqliteTable("accounts", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id").notNull().references(() => users.id),
|
||||
providerId: text("provider_id").notNull(),
|
||||
providerUserId: text("provider_user_id").notNull(),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
expiresAt: integer("expires_at", { mode: "timestamp" }),
|
||||
password: text("password"), // For credential provider
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
}, (table) => {
|
||||
return {
|
||||
userIdIdx: index("idx_accounts_user_id").on(table.userId),
|
||||
providerIdx: index("idx_accounts_provider").on(table.providerId, table.providerUserId),
|
||||
};
|
||||
});
|
||||
|
||||
// Verification tokens table
|
||||
export const verificationTokens = sqliteTable("verification_tokens", {
|
||||
id: text("id").primaryKey(),
|
||||
token: text("token").notNull().unique(),
|
||||
identifier: text("identifier").notNull(),
|
||||
type: text("type").notNull(), // email, password-reset, etc
|
||||
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
}, (table) => {
|
||||
return {
|
||||
tokenIdx: index("idx_verification_tokens_token").on(table.token),
|
||||
identifierIdx: index("idx_verification_tokens_identifier").on(table.identifier),
|
||||
};
|
||||
});
|
||||
|
||||
// Export type definitions
|
||||
export type User = z.infer<typeof userSchema>;
|
||||
export type Config = z.infer<typeof configSchema>;
|
||||
|
||||
58
src/lib/utils/auth-helpers.ts
Normal file
58
src/lib/utils/auth-helpers.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { APIRoute, APIContext } from "astro";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
/**
|
||||
* Get authenticated user from request
|
||||
* @param request - The request object from Astro API route
|
||||
* @returns The authenticated user or null if not authenticated
|
||||
*/
|
||||
export async function getAuthenticatedUser(request: Request) {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
|
||||
return session ? session.user : null;
|
||||
} catch (error) {
|
||||
console.error("Error getting session:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Require authentication for API routes
|
||||
* Returns an error response if user is not authenticated
|
||||
* @param context - The API context from Astro
|
||||
* @returns Object with user if authenticated, or error response if not
|
||||
*/
|
||||
export async function requireAuth(context: APIContext) {
|
||||
const user = await getAuthenticatedUser(context.request);
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
user: null,
|
||||
response: new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: "Unauthorized - Please log in",
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return { user, response: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user ID from authenticated session
|
||||
* @param request - The request object from Astro API route
|
||||
* @returns The user ID or null if not authenticated
|
||||
*/
|
||||
export async function getAuthenticatedUserId(request: Request): Promise<string | null> {
|
||||
const user = await getAuthenticatedUser(request);
|
||||
return user?.id || null;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { initializeRecovery, hasJobsNeedingRecovery, getRecoveryStatus } from '.
|
||||
import { startCleanupService, stopCleanupService } from './lib/cleanup-service';
|
||||
import { initializeShutdownManager, registerShutdownCallback } from './lib/shutdown-manager';
|
||||
import { setupSignalHandlers } from './lib/signal-handlers';
|
||||
import { auth } from './lib/auth';
|
||||
|
||||
// Flag to track if recovery has been initialized
|
||||
let recoveryInitialized = false;
|
||||
@@ -11,6 +12,25 @@ let cleanupServiceStarted = false;
|
||||
let shutdownManagerInitialized = false;
|
||||
|
||||
export const onRequest = defineMiddleware(async (context, next) => {
|
||||
// Handle Better Auth session
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: context.request.headers,
|
||||
});
|
||||
|
||||
if (session) {
|
||||
context.locals.user = session.user;
|
||||
context.locals.session = session.session;
|
||||
} else {
|
||||
context.locals.user = null;
|
||||
context.locals.session = null;
|
||||
}
|
||||
} catch (error) {
|
||||
// If there's an error getting the session, set to null
|
||||
context.locals.user = null;
|
||||
context.locals.session = null;
|
||||
}
|
||||
|
||||
// Initialize shutdown manager and signal handlers first
|
||||
if (!shutdownManagerInitialized) {
|
||||
try {
|
||||
|
||||
10
src/pages/api/auth/[...all].ts
Normal file
10
src/pages/api/auth/[...all].ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const ALL: APIRoute = async (ctx) => {
|
||||
// If you want to use rate limiting, make sure to set the 'x-forwarded-for' header
|
||||
// to the request headers from the context
|
||||
// ctx.request.headers.set("x-forwarded-for", ctx.clientAddress);
|
||||
|
||||
return auth.handler(ctx.request);
|
||||
};
|
||||
30
src/pages/api/auth/check-users.ts
Normal file
30
src/pages/api/auth/check-users.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { db, users } from "@/lib/db";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
try {
|
||||
const userCountResult = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(users);
|
||||
|
||||
const userCount = userCountResult[0].count;
|
||||
|
||||
if (userCount === 0) {
|
||||
return new Response(JSON.stringify({ error: "No users found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ userCount }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: "Internal server error" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
};
|
||||
13
src/pages/api/auth/legacy-backup/README.md
Normal file
13
src/pages/api/auth/legacy-backup/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Legacy Auth Routes Backup
|
||||
|
||||
These files are the original authentication routes before migrating to Better Auth.
|
||||
They are kept here as a reference during the migration process.
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- `index.ts` - Handled user session validation and getting current user
|
||||
- `login.ts` - Handled user login with email/password
|
||||
- `logout.ts` - Handled user logout and session cleanup
|
||||
- `register.ts` - Handled new user registration
|
||||
|
||||
All these endpoints are now handled by Better Auth through the catch-all route `[...all].ts`.
|
||||
@@ -2,36 +2,17 @@ import type { APIRoute } from "astro";
|
||||
import { db, organizations } from "@/lib/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { requireAuth } from "@/lib/utils/auth-helpers";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||
|
||||
export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
||||
export const PATCH: APIRoute = async (context) => {
|
||||
try {
|
||||
// Get token from Authorization header or cookies
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
const token = authHeader?.split(" ")[1] || cookies.get("token")?.value;
|
||||
// Check authentication
|
||||
const { user, response } = await requireAuth(context);
|
||||
if (response) return response;
|
||||
|
||||
if (!token) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
const userId = user!.id;
|
||||
|
||||
// Verify token and get user ID
|
||||
let userId: string;
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
||||
userId = decoded.id;
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const orgId = params.id;
|
||||
const orgId = context.params.id;
|
||||
if (!orgId) {
|
||||
return new Response(JSON.stringify({ error: "Organization ID is required" }), {
|
||||
status: 400,
|
||||
@@ -39,7 +20,7 @@ export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
||||
});
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const body = await context.request.json();
|
||||
const { destinationOrg } = body;
|
||||
|
||||
// Validate that the organization belongs to the user
|
||||
|
||||
@@ -2,36 +2,17 @@ import type { APIRoute } from "astro";
|
||||
import { db, repositories } from "@/lib/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { requireAuth } from "@/lib/utils/auth-helpers";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||
|
||||
export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
||||
export const PATCH: APIRoute = async (context) => {
|
||||
try {
|
||||
// Get token from Authorization header or cookies
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
const token = authHeader?.split(" ")[1] || cookies.get("token")?.value;
|
||||
// Check authentication
|
||||
const { user, response } = await requireAuth(context);
|
||||
if (response) return response;
|
||||
|
||||
if (!token) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
const userId = user!.id;
|
||||
|
||||
// Verify token and get user ID
|
||||
let userId: string;
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
||||
userId = decoded.id;
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const repoId = params.id;
|
||||
const repoId = context.params.id;
|
||||
if (!repoId) {
|
||||
return new Response(JSON.stringify({ error: "Repository ID is required" }), {
|
||||
status: 400,
|
||||
@@ -39,7 +20,7 @@ export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
||||
});
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const body = await context.request.json();
|
||||
const { destinationOrg } = body;
|
||||
|
||||
// Validate that the repository belongs to the user
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import App from '@/components/layout/MainLayout';
|
||||
import { db, repositories, mirrorJobs, client } from '@/lib/db';
|
||||
import { db, repositories, mirrorJobs, users } from '@/lib/db';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
|
||||
// Check if any users exist in the database
|
||||
const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`);
|
||||
const userCount = userCountResult.rows[0].count;
|
||||
const userCountResult = await db.select({ count: sql<number>`count(*)` }).from(users);
|
||||
const userCount = userCountResult[0]?.count || 0;
|
||||
|
||||
// Redirect to signup if no users exist
|
||||
if (userCount === 0) {
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
import '../styles/global.css';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { LoginForm } from '@/components/auth/LoginForm';
|
||||
import { client } from '../lib/db';
|
||||
import { db, users } from '@/lib/db';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
// Check if any users exist in the database
|
||||
const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`);
|
||||
const userCount = userCountResult.rows[0].count;
|
||||
const userCountResult = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(users);
|
||||
const userCount = userCountResult[0].count;
|
||||
|
||||
// Redirect to signup if no users exist
|
||||
if (userCount === 0) {
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
import '../styles/global.css';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { SignupForm } from '@/components/auth/SignupForm';
|
||||
import { client } from '../lib/db';
|
||||
import { db, users } from '@/lib/db';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
// Check if any users exist in the database
|
||||
const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`);
|
||||
const userCount = userCountResult.rows[0]?.count;
|
||||
const userCountResult = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(users);
|
||||
const userCount = userCountResult[0]?.count;
|
||||
|
||||
// Redirect to login if users already exist
|
||||
if (userCount !== null && Number(userCount) > 0) {
|
||||
|
||||
Reference in New Issue
Block a user