Fixing issues with Better Auth

This commit is contained in:
Arunavo Ray
2025-07-11 00:00:37 +05:30
parent b838310872
commit 6cfe43932f
23 changed files with 558 additions and 1422 deletions

View File

@@ -33,7 +33,7 @@ export function LoginForm() {
toast.success('Login successful!');
// Small delay before redirecting to see the success message
setTimeout(() => {
window.location.href = '/dashboard';
window.location.href = '/';
}, 1000);
} catch (error) {
showErrorToast(error, toast);

View File

@@ -0,0 +1,10 @@
import { LoginForm } from './LoginForm';
import Providers from '@/components/layout/Providers';
export function LoginPage() {
return (
<Providers>
<LoginForm />
</Providers>
);
}

View File

@@ -16,12 +16,11 @@ export function SignupForm() {
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;
const confirmPassword = formData.get('confirmPassword') as string | null;
if (!username || !email || !password || !confirmPassword) {
if (!email || !password || !confirmPassword) {
toast.error('Please fill in all fields');
setIsLoading(false);
return;
@@ -34,11 +33,13 @@ export function SignupForm() {
}
try {
// Derive username from email (part before @)
const username = email.split('@')[0];
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';
window.location.href = '/';
}, 1500);
} catch (error) {
showErrorToast(error, toast);
@@ -71,20 +72,6 @@ export function SignupForm() {
<CardContent>
<form id="signup-form" onSubmit={handleSignup}>
<div className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium mb-1">
Username
</label>
<input
id="username"
name="username"
type="text"
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"
disabled={isLoading}
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium mb-1">
Email
@@ -97,6 +84,7 @@ export function SignupForm() {
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 email"
disabled={isLoading}
autoFocus
/>
</div>
<div>

View File

@@ -0,0 +1,10 @@
import { SignupForm } from './SignupForm';
import Providers from '@/components/layout/Providers';
export function SignupPage() {
return (
<Providers>
<SignupForm />
</Providers>
);
}

View File

@@ -37,28 +37,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const user = betterAuthSession.data?.user || null;
const session = betterAuthSession.data || null;
// Check if this is first load and redirect if needed
useEffect(() => {
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);
}
}
};
checkFirstUser();
}, [betterAuthSession.isPending, user]);
// Don't do any redirects here - let the pages handle their own redirect logic
const login = async (email: string, password: string) => {
setIsLoading(true);
@@ -67,7 +46,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const result = await authClient.signIn.email({
email,
password,
callbackURL: "/dashboard",
callbackURL: "/",
});
if (result.error) {
@@ -93,9 +72,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const result = await authClient.signUp.email({
email,
password,
name: username, // Better Auth uses 'name' field
username, // Also pass username as additional field
callbackURL: "/dashboard",
name: username, // Better Auth uses 'name' field for display name
callbackURL: "/",
});
if (result.error) {

View File

@@ -3,13 +3,6 @@ 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({

View File

@@ -1,23 +1,22 @@
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");
}
import { db, users } from "./db";
import * as schema from "./db/schema";
import { eq } from "drizzle-orm";
export const auth = betterAuth({
// Database configuration
database: drizzleAdapter(db, {
provider: "sqlite",
usePlural: true, // Our tables use plural names (users, not user)
schema, // Pass the schema explicitly
}),
// Secret for signing tokens
secret: process.env.BETTER_AUTH_SECRET,
// Base URL configuration
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:4321",
basePath: "/api/auth", // Specify the base path for auth endpoints
// Authentication methods
@@ -30,6 +29,7 @@ export const auth = betterAuth({
console.log("Reset URL:", url);
},
},
// Session configuration
session: {
@@ -44,9 +44,8 @@ export const auth = betterAuth({
// Keep the username field from our existing schema
username: {
type: "string",
required: true,
defaultValue: "user", // Default for migration
input: true, // Allow in signup form
required: false,
input: false, // Don't show in signup form - we'll derive from email
}
},
},
@@ -56,7 +55,7 @@ export const auth = betterAuth({
// Trusted origins for CORS
trustedOrigins: [
process.env.BETTER_AUTH_URL || "http://localhost:3000",
process.env.BETTER_AUTH_URL || "http://localhost:4321",
],
});

View File

@@ -18,9 +18,9 @@ export const ENV = {
return "sqlite://data/gitea-mirror.db";
},
// JWT secret for authentication
JWT_SECRET:
process.env.JWT_SECRET || "your-secret-key-change-this-in-production",
// Better Auth secret for authentication
BETTER_AUTH_SECRET:
process.env.BETTER_AUTH_SECRET || "your-secret-key-change-this-in-production",
// Server host and port
HOST: process.env.HOST || "localhost",

View File

@@ -213,16 +213,18 @@ export const eventSchema = z.object({
export const users = sqliteTable("users", {
id: text("id").primaryKey(),
username: text("username").notNull(),
password: text("password").notNull(),
email: text("email").notNull(),
name: text("name"),
email: text("email").notNull().unique(),
emailVerified: integer("email_verified", { mode: "boolean" }).notNull().default(false),
image: text("image"),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.default(sql`(unixepoch())`),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.default(sql`(unixepoch())`),
// Custom fields
username: text("username"),
});
export const events = sqliteTable("events", {
@@ -463,9 +465,10 @@ export const sessions = sqliteTable("sessions", {
// Accounts table (for OAuth providers and credentials)
export const accounts = sqliteTable("accounts", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
userId: text("user_id").notNull().references(() => users.id),
providerId: text("provider_id").notNull(),
providerUserId: text("provider_user_id").notNull(),
providerUserId: text("provider_user_id"), // Make nullable for email/password auth
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
expiresAt: integer("expires_at", { mode: "timestamp" }),
@@ -478,6 +481,7 @@ export const accounts = sqliteTable("accounts", {
.default(sql`(unixepoch())`),
}, (table) => {
return {
accountIdIdx: index("idx_accounts_account_id").on(table.accountId),
userIdIdx: index("idx_accounts_user_id").on(table.userId),
providerIdx: index("idx_accounts_provider").on(table.providerId, table.providerUserId),
};

View File

@@ -0,0 +1,72 @@
import type { APIRoute } from "astro";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema";
import { nanoid } from "nanoid";
export const GET: APIRoute = async ({ request }) => {
try {
// Get Better Auth configuration info
const info = {
baseURL: auth.options.baseURL,
basePath: auth.options.basePath,
trustedOrigins: auth.options.trustedOrigins,
emailPasswordEnabled: auth.options.emailAndPassword?.enabled,
userFields: auth.options.user?.additionalFields,
databaseConfig: {
usePlural: true,
provider: "sqlite"
}
};
return new Response(JSON.stringify({
success: true,
config: info
}), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
return new Response(JSON.stringify({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
}), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
};
export const POST: APIRoute = async ({ request }) => {
try {
// Test creating a user directly
const userId = nanoid();
const now = new Date();
await db.insert(users).values({
id: userId,
email: "test2@example.com",
emailVerified: false,
username: "test2",
// Let the database handle timestamps with defaults
});
return new Response(JSON.stringify({
success: true,
userId,
message: "User created successfully"
}), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
return new Response(JSON.stringify({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
details: error
}), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
};

View File

@@ -1,7 +1,7 @@
---
import '../styles/global.css';
import ThemeScript from '@/components/theme/ThemeScript.astro';
import { LoginForm } from '@/components/auth/LoginForm';
import { LoginPage } from '@/components/auth/LoginPage';
import { db, users } from '@/lib/db';
import { sql } from 'drizzle-orm';
@@ -30,7 +30,7 @@ const generator = Astro.generator;
</head>
<body>
<div class="h-dvh flex items-center justify-center bg-muted/30 p-4">
<LoginForm client:load />
<LoginPage client:load />
</div>
</body>
</html>

View File

@@ -1,7 +1,7 @@
---
import '../styles/global.css';
import ThemeScript from '@/components/theme/ThemeScript.astro';
import { SignupForm } from '@/components/auth/SignupForm';
import { SignupPage } from '@/components/auth/SignupPage';
import { db, users } from '@/lib/db';
import { sql } from 'drizzle-orm';
@@ -34,7 +34,7 @@ const generator = Astro.generator;
<h1 class="text-3xl font-bold mb-2">Welcome to Gitea Mirror</h1>
<p class="text-muted-foreground">Let's set up your administrator account to get started.</p>
</div>
<SignupForm client:load />
<SignupPage client:load />
</div>
</body>
</html>