Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions app/api/register/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { eq } from "drizzle-orm";

export async function POST(req: Request) {
try {
const { name, email, password } = await req.json();
const { name, email: rawEmail, password } = await req.json();

if (!email || !password) {
if (!rawEmail || !password) {
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
}

const email = rawEmail.toLowerCase();
const [existingUser] = await db.select().from(users).where(eq(users.email, email)).limit(1);

if (existingUser) {
Expand All @@ -20,12 +21,13 @@ export async function POST(req: Request) {

const hashedPassword = await bcrypt.hash(password, 10);

const adminEmail = process.env.ADMIN_EMAIL?.toLowerCase();
const [user] = await db.insert(users).values({
name,
email,
password: hashedPassword,
status: "PENDING",
role: email === process.env.ADMIN_EMAIL ? "ADMIN" : "USER",
role: email === adminEmail ? "ADMIN" : "USER",
}).returning();

return NextResponse.json({ user: { email: user.email, name: user.name } });
Expand Down
81 changes: 47 additions & 34 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState, Suspense } from "react";
import { signIn } from "next-auth/react";
import { useState, Suspense, useEffect } from "react";
import { signIn, getProviders } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";

Expand All @@ -10,6 +10,15 @@ function LoginContent() {
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [providers, setProviders] = useState<any>(null);

useEffect(() => {
const fetchProviders = async () => {
const p = await getProviders();
setProviders(p);
};
fetchProviders();
}, []);
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/dashboard";
Expand Down Expand Up @@ -51,39 +60,43 @@ function LoginContent() {
</div>

<div className="mt-8 space-y-6">
<button
onClick={handleGoogleSignIn}
className="w-full flex items-center justify-center gap-3 py-3 px-4 bg-white text-gray-900 rounded-lg font-semibold hover:bg-gray-100 transition-all shadow-md"
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</button>
{providers?.google && (
<>
<button
onClick={handleGoogleSignIn}
className="w-full flex items-center justify-center gap-3 py-3 px-4 bg-white text-gray-900 rounded-lg font-semibold hover:bg-gray-100 transition-all shadow-md"
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
fill="#4285F4"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="#34A853"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="#FBBC05"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
/>
<path
fill="#EA4335"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</button>

<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-primary/30"></span>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-background-alt text-foreground-muted">Or continue with email</span>
</div>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-primary/30"></span>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-background-alt text-foreground-muted">Or continue with email</span>
</div>
</div>
</>
)}

<form className="space-y-4" onSubmit={handleSubmit}>
{error && (
Expand Down
88 changes: 0 additions & 88 deletions dev_server.log

This file was deleted.

33 changes: 22 additions & 11 deletions lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import bcrypt from "bcryptjs";

export const authOptions: NextAuthOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}),
...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
? [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
]
: []),
CredentialsProvider({
name: "credentials",
credentials: {
Expand All @@ -23,10 +27,12 @@ export const authOptions: NextAuthOptions = {
throw new Error("Invalid credentials");
}

const isAdmin = credentials.email === process.env.ADMIN_EMAIL;
const inputEmail = credentials.email.toLowerCase();
const adminEmail = process.env.ADMIN_EMAIL?.toLowerCase();
const isAdmin = inputEmail === adminEmail;
const adminPasswordEnv = process.env.ADMIN_PASSWORD;

const [existingUser] = await db.select().from(users).where(eq(users.email, credentials.email)).limit(1);
const [existingUser] = await db.select().from(users).where(eq(users.email, inputEmail)).limit(1);
let user = existingUser;

// 1. Try DB password first (important if changed via UI)
Expand All @@ -49,7 +55,7 @@ export const authOptions: NextAuthOptions = {
if (isAdmin && adminPasswordEnv && credentials.password === adminPasswordEnv) {
if (!user) {
const [newUser] = await db.insert(users).values({
email: credentials.email,
email: inputEmail,
password: await bcrypt.hash(adminPasswordEnv, 10),
role: "ADMIN",
status: "APPROVED",
Expand Down Expand Up @@ -91,12 +97,16 @@ export const authOptions: NextAuthOptions = {
callbacks: {
async signIn({ user, account }) {
if (account?.provider === "google") {
const [existingUser] = await db.select().from(users).where(eq(users.email, user.email!)).limit(1);
const userEmail = user.email?.toLowerCase();
if (!userEmail) return false;

const [existingUser] = await db.select().from(users).where(eq(users.email, userEmail)).limit(1);

if (!existingUser) {
const isAdmin = user.email === process.env.ADMIN_EMAIL;
const adminEmail = process.env.ADMIN_EMAIL?.toLowerCase();
const isAdmin = userEmail === adminEmail;
await db.insert(users).values({
email: user.email!,
email: userEmail,
name: user.name,
image: user.image,
status: isAdmin ? "APPROVED" : "PENDING",
Expand All @@ -108,7 +118,8 @@ export const authOptions: NextAuthOptions = {
},
async jwt({ token, user }) {
if (user) {
const [dbUser] = await db.select().from(users).where(eq(users.email, user.email!)).limit(1);
const userEmail = user.email?.toLowerCase();
const [dbUser] = await db.select().from(users).where(eq(users.email, userEmail!)).limit(1);
if (dbUser) {
token.id = dbUser.id;
token.role = dbUser.role;
Expand Down
3 changes: 2 additions & 1 deletion scripts/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ function main() {
}

const defaultEnv = {
NEXTAUTH_URL: 'http://localhost:3000',
NEXTAUTH_SECRET: crypto.randomBytes(32).toString('hex'),
DATABASE_URL: 'file:./dev.db',
ADMIN_EMAIL: 'admin@virtuehearts.org',
ADMIN_PASSWORD: 'InitialAdminPassword123!',
GOOGLE_CLIENT_ID: '',
GOOGLE_CLIENT_SECRET: '',
OPENROUTER_API_KEY: 'sk-or-v1-placeholder',
};

Expand Down