20260414tue
Next.js + Supabase で SSR 認証を組み込むための構成メモ。
server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(cookie =>
cookieStore.set(cookie.name, cookie.value, cookie.options)
);
} catch {}
},
},
}
);
}
client.ts
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
);
}
middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
);
supabaseResponse = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
},
},
}
);
const {
data: { user },
} = await supabase.auth.getUser();
if (!user && request.nextUrl.pathname.startsWith("/admin")) {
// admin/login自体はリダイレクトしない
if (request.nextUrl.pathname === "/admin/login") {
return supabaseResponse;
}
const url = request.nextUrl.clone();
url.pathname = "/admin/login";
return NextResponse.redirect(url);
}
return supabaseResponse;
}
export const config = {
matcher: ["/admin/:path*"],
};
login.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { createClient } from "@/app/utils/supabase/client";
export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const router = useRouter();
const supabase = createClient();
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
setError(error.message);
return;
}
router.push("/admin/posts/new");
}
return (
<main
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
}}
>
<form
onSubmit={handleSubmit}
style={{
background: "var(--nvim-surface)",
border: "1px solid var(--nvim-border)",
borderLeft: "3px solid var(--nvim-green)",
padding: "40px",
width: "100%",
maxWidth: "400px",
}}
>
<div className="comment-block" style={{ marginBottom: "24px" }}>
<span>-- LOGIN --</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
<div>
<label
style={{
fontSize: "12px",
color: "var(--nvim-muted)",
display: "block",
marginBottom: "6px",
}}
>
email
</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
style={{
width: "100%",
background: "var(--nvim-bg)",
border: "1px solid var(--nvim-border)",
color: "var(--nvim-text)",
padding: "8px 12px",
fontSize: "14px",
outline: "none",
}}
/>
</div>
<div>
<label
style={{
fontSize: "12px",
color: "var(--nvim-muted)",
display: "block",
marginBottom: "6px",
}}
>
password
</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
style={{
width: "100%",
background: "var(--nvim-bg)",
border: "1px solid var(--nvim-border)",
color: "var(--nvim-text)",
padding: "8px 12px",
fontSize: "14px",
outline: "none",
}}
/>
</div>
{error && (
<p style={{ fontSize: "12px", color: "#E06C75" }}>{error}</p>
)}
<button
type="submit"
style={{
background: "var(--nvim-green)",
color: "var(--nvim-bg)",
border: "none",
padding: "10px",
fontSize: "13px",
fontWeight: 700,
cursor: "pointer",
fontFamily: "inherit",
letterSpacing: "0.1em",
}}
>
:wq LOGIN
</button>
</div>
</form>
</main>
);
}