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:
@@ -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,
|
||||
|
||||
@@ -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
23
src/lib/supabase/admin.ts
Normal 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" },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user