wp-login: use service_role admin client for createUser; /login redirects to /client-login

- Add src/lib/supabase/admin.ts: server-only Supabase client constructed
  with SUPABASE_SERVICE_ROLE_KEY. Uses persistSession=false. Throws if
  the env var is missing.
- src/app/api/auth/wp-login/route.ts: switch the auth.admin.createUser
  call (step 5, transparent migration on first login) to use the new
  admin client. The anon-key client cannot call admin.createUser, which
  would have made first-login attempts for legacy WP users fail at
  step 5 with a permission error.
- src/app/login/page.tsx: replaced the old WP-style login form with a
  server-side redirect to /client-login so the new client-login is the
  canonical PR-firm/contributor entry point. Existing bookmarks for
  /login keep working.

Coolify app env: SUPABASE_SERVICE_ROLE_KEY added (set out-of-band; not
NEXT_PUBLIC_-prefixed so it never ships to the client bundle).
This commit is contained in:
Ryan Salazar
2026-05-08 20:21:08 +00:00
parent 66ff0f08ae
commit 2b33713c3e
3 changed files with 32 additions and 175 deletions

View File

@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { createAdminClient } from '@/lib/supabase/admin';
import { validatePhpassPassword } from '@/lib/auth/phpass';
export async function POST(request: NextRequest) {
@@ -54,8 +55,10 @@ export async function POST(request: NextRequest) {
}
}
// 5. Create a new Supabase auth account for this legacy user (transparent migration)
const { data: signUpData, error: signUpError } = await supabase.auth.admin.createUser({
// 5. Create a new Supabase auth account for this legacy user (transparent migration).
// admin.createUser requires the service_role key — use the admin client.
const admin = createAdminClient();
const { data: signUpData, error: signUpError } = await admin.auth.admin.createUser({
email: email.toLowerCase().trim(),
password,
email_confirm: true,

View File

@@ -1,176 +1,7 @@
"use client";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { createClient } from "@/lib/supabase/client";
import { redirect } from "next/navigation";
// /login is an alias for the canonical PR Firm / contributor login at
// /client-login. Server-side redirect so any existing bookmarks update.
export default function LoginPage() {
const router = useRouter();
const supabase = createClient();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [checkingAuth, setCheckingAuth] = useState(true);
useEffect(() => {
supabase.auth.getUser().then(({ data }) => {
if (data?.user) {
router.replace("/account");
} else {
setCheckingAuth(false);
}
});
}, []);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!email || !password) {
setError("Please enter your email and password.");
return;
}
setLoading(true);
setError(null);
try {
const { error: signInError } = await supabase.auth.signInWithPassword({ email, password });
if (signInError) {
setError(signInError.message);
} else {
router.push("/account");
router.refresh();
}
} catch {
setError("An unexpected error occurred. Please try again.");
} finally {
setLoading(false);
}
};
if (checkingAuth) {
return (
<div className="min-h-screen bg-[#111111]">
<Header />
<div className="flex items-center justify-center py-24">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
</div>
<Footer />
</div>
);
}
return (
<div className="min-h-screen bg-[#111111]">
<Header />
{/* Breadcrumb */}
<div className="bg-[#0d0d0d] border-b border-[#1e1e1e]">
<div className="max-w-container mx-auto px-4 py-2.5">
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-xs font-body text-[#555]">
<Link href="/home-page" className="hover:text-[#3b82f6] transition-colors">Home</Link>
<span aria-hidden="true">/</span>
<span className="text-[#888]">Sign In</span>
</nav>
</div>
</div>
<main className="max-w-container mx-auto px-4 py-12 md:py-16">
<div className="max-w-md mx-auto">
{/* Header */}
<div className="text-center mb-8">
<h1 className="font-heading text-2xl md:text-3xl font-bold text-[#e8e8e8] mb-2">
Sign In
</h1>
<p className="font-body text-sm text-[#666]">
Access your bookmarks, reading history, and personalized content.
</p>
</div>
{/* Form Card */}
<div className="bg-[#161616] border border-[#2a2a2a] rounded-sm p-6 md:p-8">
<form onSubmit={handleLogin} className="space-y-5" noValidate>
{/* Error */}
{error && (
<div className="bg-[#1a0a0a] border border-[#cc0000] text-[#ff6b6b] font-body text-sm px-4 py-3 rounded-sm flex items-start gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="mt-0.5 flex-shrink-0" aria-hidden="true">
<circle cx="12" cy="12" r="10" /><line x1="15" y1="9" x2="9" y2="15" /><line x1="9" y1="9" x2="15" y2="15" />
</svg>
{error}
</div>
)}
{/* Email */}
<div>
<label htmlFor="email" className="block font-body text-xs font-bold uppercase tracking-wider text-[#888] mb-1.5">
Email Address
</label>
<input
id="email"
type="email"
autoComplete="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
disabled={loading}
className="w-full h-10 px-3 bg-[#0d0d0d] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm rounded-sm placeholder-[#444] focus:outline-none focus:border-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors disabled:opacity-50"
/>
</div>
{/* Password */}
<div>
<div className="flex items-center justify-between mb-1.5">
<label htmlFor="password" className="block font-body text-xs font-bold uppercase tracking-wider text-[#888]">
Password
</label>
</div>
<input
id="password"
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
disabled={loading}
className="w-full h-10 px-3 bg-[#0d0d0d] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm rounded-sm placeholder-[#444] focus:outline-none focus:border-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors disabled:opacity-50"
/>
</div>
{/* Submit */}
<button
type="submit"
disabled={loading}
className="w-full h-10 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body text-sm font-bold uppercase tracking-wider rounded-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2">
{loading ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Signing In...
</>
) : (
"Sign In"
)}
</button>
</form>
{/* Divider */}
<div className="flex items-center gap-3 my-6">
<div className="flex-1 h-px bg-[#2a2a2a]" />
<span className="font-body text-xs text-[#555]">New to BroadcastBeat?</span>
<div className="flex-1 h-px bg-[#2a2a2a]" />
</div>
{/* Register Link */}
<Link
href="/register"
className="block w-full h-10 border border-[#2a2a2a] hover:border-[#3b82f6] text-[#888] hover:text-[#3b82f6] font-body text-sm font-bold uppercase tracking-wider rounded-sm transition-colors flex items-center justify-center">
Create an Account
</Link>
</div>
</div>
</main>
<Footer />
</div>
);
redirect("/client-login");
}

23
src/lib/supabase/admin.ts Normal file
View File

@@ -0,0 +1,23 @@
import { createClient } from "@supabase/supabase-js";
/**
* Server-only Supabase client authenticated as `service_role`. Used for the
* admin auth API (createUser, deleteUser, listUsers). Bypasses RLS — never
* import this from client components.
*
* Needs SUPABASE_SERVICE_ROLE_KEY in the env (set as a Coolify app env var,
* NOT prefixed with NEXT_PUBLIC_ so it never ships to the client bundle).
*/
export function createAdminClient() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!url || !key) {
throw new Error(
"createAdminClient: missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY",
);
}
return createClient(url, key, {
auth: { persistSession: false, autoRefreshToken: false },
db: { schema: process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb" },
});
}