Skip to content

20260414tue

Published: at 15:00

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>
  );
}