diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml new file mode 100644 index 0000000..9383b15 --- /dev/null +++ b/.github/workflows/nextjs.yml @@ -0,0 +1,96 @@ +# Sample workflow for building and deploying a Next.js site to GitHub Pages +# +# To get started with Next.js see: https://nextjs.org/docs/getting-started +# +name: Deploy Next.js site to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["preview"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Detect package manager + id: detect-package-manager + run: | + if [ -f "${{ github.workspace }}/yarn.lock" ]; then + echo "manager=yarn" >> $GITHUB_OUTPUT + echo "command=install" >> $GITHUB_OUTPUT + echo "runner=yarn" >> $GITHUB_OUTPUT + exit 0 + elif [ -f "${{ github.workspace }}/package.json" ]; then + echo "manager=npm" >> $GITHUB_OUTPUT + echo "command=ci" >> $GITHUB_OUTPUT + echo "runner=npx --no-install" >> $GITHUB_OUTPUT + exit 0 + else + echo "Unable to determine package manager" + exit 1 + fi + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: ${{ steps.detect-package-manager.outputs.manager }} + - name: Setup Pages + uses: actions/configure-pages@v5 + with: + # Automatically inject basePath in your Next.js configuration file and disable + # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). + # + # You may remove this line if you want to manage the configuration yourself. + static_site_generator: next + - name: Restore cache + uses: actions/cache@v4 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- + - name: Install dependencies + run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} + - name: Build with Next.js + run: ${{ steps.detect-package-manager.outputs.runner }} next build + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./out + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/next.config.ts b/next.config.ts index 5f15bb0..7dad87a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,7 +2,10 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ + output: "export", + trailingSlash: true, images: { + unoptimized: true, remotePatterns: [ { protocol: "https", diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index 3cc4d83..0000000 --- a/src/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { handlers } from "../../../../../auth"; -export const { GET, POST } = handlers; diff --git a/src/app/api/create-checkout-session/route.ts b/src/app/api/create-checkout-session/route.ts deleted file mode 100644 index 58a7621..0000000 --- a/src/app/api/create-checkout-session/route.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import type { Stripe } from "stripe"; -import { auth } from "../../../../auth"; -import { checkUserPurchase } from "@/lib/supabase/utils"; -import { CURRENT_VERSION, METADATA_KEYS, stripe } from "@/lib/stripe"; - -// コンテンツ購入用のStripeセッションを作成 -async function createCheckoutSession( - userIdentifier: string, - contentId: string, - title: string, - price: number, - contentType: "book" | "article" -): Promise { - const APP_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL; - if (!APP_BASE_URL) { - throw new Error("BASE_URL が設定されていません"); - } - - const successUrl = - contentType === "book" - ? `${APP_BASE_URL}/books/${contentId}?success=true` - : `${APP_BASE_URL}/posts/${contentId}?success=true`; - - const sessionParams: Stripe.Checkout.SessionCreateParams = { - payment_method_types: ["card"], - line_items: [ - { - price_data: { - currency: "jpy", - product_data: { - name: title, - description: `コンテンツID: ${contentId}`, - }, - unit_amount: price, - }, - quantity: 1, - }, - ], - mode: "payment", - // webhookで使用するためのメタデータを記載 - metadata: { - [METADATA_KEYS.USER_IDENTIFIER]: userIdentifier, - [METADATA_KEYS.CONTENT_ID]: contentId, - [METADATA_KEYS.PRICE]: price.toString(), - [METADATA_KEYS.VERSION]: CURRENT_VERSION, - }, - success_url: successUrl, - cancel_url: APP_BASE_URL, - }; - - return stripe.checkout.sessions.create(sessionParams); -} - -type RequestData = { - contentId: string; - price: number; - title: string; - contentType: "book" | "article"; -}; - -export async function POST(req: NextRequest) { - try { - // Auth.jsからセッション情報を取得 - const session = await auth(); - - if (!session || !session.user) { - return new NextResponse("認証が必要です", { status: 401 }); - } - - const { contentId, price, title, contentType }: RequestData = - await req.json(); - - if (!contentId || !price || !title || !contentType) { - return new NextResponse("必要な情報が不足しています", { status: 400 }); - } - - // ユーザー識別子を取得 (GitHubのID) - const userIdentifier = session.user.id; - - if (!userIdentifier) { - return new NextResponse("ユーザー識別子が取得できません", { - status: 400, - }); - } - - // すでに購入済みの場合、リダイレクトさせる - const existingPurchase = await checkUserPurchase(userIdentifier, contentId); - // 既存の購入記録があれば処理をスキップ - if (existingPurchase) { - console.log( - `既に購入済み: ユーザー=${userIdentifier}, コンテンツ=${contentId}` - ); - return NextResponse.json({ - url: - contentType === "book" - ? `${process.env.NEXT_PUBLIC_BASE_URL}/books/${contentId}` - : `${process.env.NEXT_PUBLIC_BASE_URL}/posts/${contentId}`, - }); - } - - // Stripeのセッションを作成 - const stripeSession = await createCheckoutSession( - userIdentifier!, - contentId, - title, - price, - contentType - ); - - // チェックアウトURLを返す(クライアント側でリダイレクト) - return NextResponse.json({ - sessionId: stripeSession.id, - url: stripeSession.url, - }); - } catch (error) { - console.error("Checkout session error:", error); - return new NextResponse("内部エラーが発生しました", { status: 500 }); - } -} diff --git a/src/app/api/webhooks/stripe/route.ts b/src/app/api/webhooks/stripe/route.ts deleted file mode 100644 index a646bee..0000000 --- a/src/app/api/webhooks/stripe/route.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import type { Stripe } from "stripe"; -import { checkUserPurchase, createPurchaseRecord } from "@/lib/supabase/utils"; -import { METADATA_KEYS, stripe } from "@/lib/stripe"; - -// 購入完了時にSupabaseに記録する -async function recordCompletedPurchase(session: Stripe.Checkout.Session) { - const metadata = session.metadata || {}; - - // メタデータからデータを取得 - const userIdentifier = metadata[METADATA_KEYS.USER_IDENTIFIER]; - const contentId = metadata[METADATA_KEYS.CONTENT_ID]; - const price = parseInt(metadata[METADATA_KEYS.PRICE] || "0", 10); - - if (!userIdentifier || !contentId || isNaN(price)) { - throw new Error("必要なメタデータが不足しています"); - } - - // 支払い状態のチェック - if (session.payment_status !== "paid") { - console.log( - `支払いがまだ完了していません: ${session.id}, status=${session.payment_status}` - ); - return false; - } - - // payment_intentの取得 - let paymentIntentId: string | null = null; - if (typeof session.payment_intent === "string") { - paymentIntentId = session.payment_intent; - } else if ( - session.payment_intent && - typeof session.payment_intent === "object" - ) { - paymentIntentId = session.payment_intent.id; - } - - const existingPurchase = await checkUserPurchase(userIdentifier, contentId); - // 既存の購入記録があれば処理をスキップ - if (existingPurchase) { - console.log( - `既に購入済み: ユーザー=${userIdentifier}, コンテンツ=${contentId}` - ); - return true; - } - - // 新規購入を記録 - const data = await createPurchaseRecord({ - userIdentifier, - contentId, - paymentIntentId: paymentIntentId || "", - amount: price, - }); - - return !!data; -} - -export async function POST(req: NextRequest) { - try { - const signature = req.headers.get("stripe-signature"); - if (!signature) { - return new NextResponse("Stripe署名がありません", { status: 401 }); - } - - const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; - if (!webhookSecret) { - throw new Error("STRIPE_WEBHOOK_SECRET環境変数が設定されていません"); - } - - const body = await req.text(); - - let event: Stripe.Event; - try { - event = await stripe.webhooks.constructEventAsync( - body, - signature, - webhookSecret - ); - } catch (error) { - const message = error instanceof Error ? error.message : "不明なエラー"; - console.error(`Webhook署名検証失敗: ${message}`); - return new NextResponse(`Webhook Error: ${message}`, { status: 400 }); - } - - // イベントタイプに基づく処理 - switch (event.type) { - case "checkout.session.completed": - const session = event.data.object as Stripe.Checkout.Session; - - // 支払いが完了した時点でSupabaseに記録する - await recordCompletedPurchase(session); - break; - - default: - console.log(`未処理のイベントタイプ: ${event.type}`); - } - - return new NextResponse(null, { status: 200 }); - } catch (error) { - const message = error instanceof Error ? error.message : "不明なエラー"; - console.error(`Webhook処理エラー: ${message}`); - return new NextResponse(`Webhook Error: ${message}`, { status: 500 }); - } -} diff --git a/src/app/books/[bookSlug]/[chapterSlug]/page.tsx b/src/app/books/[bookSlug]/[chapterSlug]/page.tsx index 9afde2b..19e7de7 100644 --- a/src/app/books/[bookSlug]/[chapterSlug]/page.tsx +++ b/src/app/books/[bookSlug]/[chapterSlug]/page.tsx @@ -4,7 +4,7 @@ import { fetchBook, fetchBooks } from "@/lib/github"; import { Button } from "@/components/ui/button"; import { ChevronLeft, ChevronRight, BookOpen } from "lucide-react"; import TableOfContents from "./_components/table-of-contents"; -import { checkAccessAndRedirect } from "@/lib/supabase/utils"; +// import { checkAccessAndRedirect } from "@/lib/supabase/utils"; export async function generateStaticParams() { // すべての本を取得 @@ -38,9 +38,9 @@ export default async function ChapterPage({ params }: ChapterPageProps) { notFound(); } - if (book.isPaid) { - await checkAccessAndRedirect(book.id, "book"); - } + // if (book.isPaid) { + // await checkAccessAndRedirect(book.id, "book"); + // } // データ一覧から、現在のチャプターを探す const chapterIndex = book.chapters.findIndex( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 12e22c6..53611a8 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import Header from "@/components/header"; +import PreviewBanner from "@/components/preview-banner"; const inter = Inter({ subsets: ["latin"] }); @@ -18,6 +19,7 @@ export default function RootLayout({ return ( +
{children}
diff --git a/src/app/posts/[slug]/page.tsx b/src/app/posts/[slug]/page.tsx index 4979a1c..2c0f88a 100644 --- a/src/app/posts/[slug]/page.tsx +++ b/src/app/posts/[slug]/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { fetchArticles } from "@/lib/github"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { checkAccessAndRedirect } from "@/lib/supabase/utils"; +// import { checkAccessAndRedirect } from "@/lib/supabase/utils"; // ビルド時に動的なページを生成 export async function generateStaticParams() { @@ -32,9 +32,9 @@ export default async function PostDetailPage({ params }: PostDetailPageProps) { ); } - if (post.isPaid) { - await checkAccessAndRedirect(post.id, "article"); - } + // if (post.isPaid) { + // await checkAccessAndRedirect(post.id, "article"); + // } return (
diff --git a/src/components/header.tsx b/src/components/header.tsx index 9b4a3d4..5508661 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,10 +1,10 @@ import Link from "next/link"; -import { auth } from "../../auth"; -import UserAvatar from "./user-avatar"; +// import { auth } from "../../auth"; +// import UserAvatar from "./user-avatar"; import SignInButton from "./sign-in-button"; async function Header() { - const session = await auth(); + // const session = await auth(); return ( diff --git a/src/components/preview-banner.tsx b/src/components/preview-banner.tsx new file mode 100644 index 0000000..8b9a472 --- /dev/null +++ b/src/components/preview-banner.tsx @@ -0,0 +1,10 @@ +export default function PreviewBanner() { + return ( +
+

+ 📖 これはプレビュー版です。実際の認証・決済機能は含まれていません。 + GitHub Pages で公開中 +

+
+ ); +} diff --git a/src/components/purchase-button.tsx b/src/components/purchase-button.tsx index fbf6e8e..f6ef613 100644 --- a/src/components/purchase-button.tsx +++ b/src/components/purchase-button.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +// import { useState } from "react"; import { Button } from "./ui/button"; type PurchaseButtonProps = { @@ -13,50 +13,50 @@ type PurchaseButtonProps = { }; export default function PurchaseButton(props: PurchaseButtonProps) { - const [loading, setLoading] = useState(false); + // const [loading, setLoading] = useState(false); - const handlePurchase = async () => { - try { - setLoading(true); - const response = await fetch("/api/create-checkout-session", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - contentId: props.contentId, - price: props.price, - title: props.title, - contentType: props.contentType, - }), - }); - if (response.status === 401) { - alert("認証が必要です"); - window.location.href = "/api/auth/signin"; - return; - } - if (!response.ok) { - throw new Error("購入処理エラー"); - } + // const handlePurchase = async () => { + // try { + // setLoading(true); + // const response = await fetch("/api/create-checkout-session", { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // body: JSON.stringify({ + // contentId: props.contentId, + // price: props.price, + // title: props.title, + // contentType: props.contentType, + // }), + // }); + // if (response.status === 401) { + // alert("認証が必要です"); + // window.location.href = "/api/auth/signin"; + // return; + // } + // if (!response.ok) { + // throw new Error("購入処理エラー"); + // } - const data = await response.json(); - window.location.href = data.url; - } catch (error) { - console.error(error); - alert("購入処理中にエラーが発生しました。"); - } finally { - setLoading(false); - } - }; + // const data = await response.json(); + // window.location.href = data.url; + // } catch (error) { + // console.error(error); + // alert("購入処理中にエラーが発生しました。"); + // } finally { + // setLoading(false); + // } + // }; return ( ); } diff --git a/src/components/sign-in-button.tsx b/src/components/sign-in-button.tsx index aa8c86c..283f2a4 100644 --- a/src/components/sign-in-button.tsx +++ b/src/components/sign-in-button.tsx @@ -1,4 +1,4 @@ -import { signIn } from "../../auth"; +// import { signIn } from "../../auth"; import { Button } from "./ui/button"; interface SignInButtonProps { @@ -9,15 +9,15 @@ interface SignInButtonProps { export default function SignInButton(props: SignInButtonProps) { return ( -
{ - "use server"; - await signIn("github"); - }} - > - -
+ //
{ + // "use server"; + // await signIn("github"); + // }} + // > + + //
); } diff --git a/src/lib/stripe.ts b/src/lib/stripe.ts index bb8c41f..908c2b8 100644 --- a/src/lib/stripe.ts +++ b/src/lib/stripe.ts @@ -1,19 +1,19 @@ -import Stripe from "stripe"; +// import Stripe from "stripe"; -if (!process.env.STRIPE_SECRET_KEY) { - throw new Error("STRIPE_SECRET_KEY is not defined"); -} +// if (!process.env.STRIPE_SECRET_KEY) { +// throw new Error("STRIPE_SECRET_KEY is not defined"); +// } -export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { - apiVersion: "2025-02-24.acacia", -}); +// export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { +// apiVersion: "2025-02-24.acacia", +// }); -// 購入記録に含めるメタデータのキー -export const METADATA_KEYS = { - USER_IDENTIFIER: "user-identifier", - CONTENT_ID: "content-id", - PRICE: "price", - VERSION: "version", -} as const; +// // 購入記録に含めるメタデータのキー +// export const METADATA_KEYS = { +// USER_IDENTIFIER: "user-identifier", +// CONTENT_ID: "content-id", +// PRICE: "price", +// VERSION: "version", +// } as const; -export const CURRENT_VERSION = "v1"; +// export const CURRENT_VERSION = "v1"; diff --git a/src/lib/supabase/client.ts b/src/lib/supabase/client.ts index 79b2d44..b170585 100644 --- a/src/lib/supabase/client.ts +++ b/src/lib/supabase/client.ts @@ -1,24 +1,24 @@ -import { Database } from "@/types/database.types"; -import { createClient } from "@supabase/supabase-js"; +// import { Database } from "@/types/database.types"; +// import { createClient } from "@supabase/supabase-js"; -if (!process.env.NEXT_PUBLIC_SUPABASE_URL) { - throw new Error("NEXT_PUBLIC_SUPABASE_URL is not defined"); -} +// if (!process.env.NEXT_PUBLIC_SUPABASE_URL) { +// throw new Error("NEXT_PUBLIC_SUPABASE_URL is not defined"); +// } -if (!process.env.SUPABASE_SERVICE_ROLE_KEY) { - throw new Error("SUPABASE_SERVICE_ROLE_KEY is not defined"); -} +// if (!process.env.SUPABASE_SERVICE_ROLE_KEY) { +// throw new Error("SUPABASE_SERVICE_ROLE_KEY is not defined"); +// } -// サービスロールを使ったクライアント -const supabaseAdmin = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL, - process.env.SUPABASE_SERVICE_ROLE_KEY, - { - auth: { - autoRefreshToken: false, - persistSession: false, - }, - } -); +// // サービスロールを使ったクライアント +// const supabaseAdmin = createClient( +// process.env.NEXT_PUBLIC_SUPABASE_URL, +// process.env.SUPABASE_SERVICE_ROLE_KEY, +// { +// auth: { +// autoRefreshToken: false, +// persistSession: false, +// }, +// } +// ); -export { supabaseAdmin }; +// export { supabaseAdmin }; diff --git a/src/lib/supabase/utils.ts b/src/lib/supabase/utils.ts index 0d48927..1a68e72 100644 --- a/src/lib/supabase/utils.ts +++ b/src/lib/supabase/utils.ts @@ -1,83 +1,83 @@ -import { redirect } from "next/navigation"; -import { auth } from "../../../auth"; -import { supabaseAdmin } from "./client"; +// import { redirect } from "next/navigation"; +// import { auth } from "../../../auth"; +// import { supabaseAdmin } from "./client"; -interface CreatePurchaseParams { - userIdentifier: string; - contentId: string; - paymentIntentId: string; - amount: number; -} +// interface CreatePurchaseParams { +// userIdentifier: string; +// contentId: string; +// paymentIntentId: string; +// amount: number; +// } -/** - * 購入履歴を記録する - */ -export async function createPurchaseRecord({ - userIdentifier, - contentId, - paymentIntentId, - amount, -}: CreatePurchaseParams) { - const { data, error } = await supabaseAdmin - .from("purchases") - .insert({ - user_identifier: userIdentifier, - content_id: contentId, - stripe_payment_intent_id: paymentIntentId, - amount, - }) - .select() - .single(); +// /** +// * 購入履歴を記録する +// */ +// export async function createPurchaseRecord({ +// userIdentifier, +// contentId, +// paymentIntentId, +// amount, +// }: CreatePurchaseParams) { +// const { data, error } = await supabaseAdmin +// .from("purchases") +// .insert({ +// user_identifier: userIdentifier, +// content_id: contentId, +// stripe_payment_intent_id: paymentIntentId, +// amount, +// }) +// .select() +// .single(); - if (error) { - console.error("購入記録エラー:", error); - throw error; - } +// if (error) { +// console.error("購入記録エラー:", error); +// throw error; +// } - return data; -} +// return data; +// } -/** - * ユーザーの既存の購入記録をチェック - */ -export async function checkUserPurchase( - userIdentifier: string, - contentId: string -) { - try { - const { data } = await supabaseAdmin - .from("purchases") - .select("id") - .eq("user_identifier", userIdentifier) - .eq("content_id", contentId) - .maybeSingle(); +// /** +// * ユーザーの既存の購入記録をチェック +// */ +// export async function checkUserPurchase( +// userIdentifier: string, +// contentId: string +// ) { +// try { +// const { data } = await supabaseAdmin +// .from("purchases") +// .select("id") +// .eq("user_identifier", userIdentifier) +// .eq("content_id", contentId) +// .maybeSingle(); - return !!data; - } catch (error) { - console.error("購入確認エラー:", error); - throw error; - } -} +// return !!data; +// } catch (error) { +// console.error("購入確認エラー:", error); +// throw error; +// } +// } -/** - * アクセス時に、有料コンテンツを購入していない場合はリダイレクト - */ -export async function checkAccessAndRedirect( - contentId: string, - contentType: "book" | "article" -) { - // ログインの確認 - const session = await auth(); - if (!session?.user) redirect("/api/auth/signin"); +// /** +// * アクセス時に、有料コンテンツを購入していない場合はリダイレクト +// */ +// export async function checkAccessAndRedirect( +// contentId: string, +// contentType: "book" | "article" +// ) { +// // ログインの確認 +// const session = await auth(); +// if (!session?.user) redirect("/api/auth/signin"); - // 購入の確認 - const hasPurchased = await checkUserPurchase(session.user.id!, contentId); - if (!hasPurchased) { - const redirectUrl = - contentType === "book" - ? `${process.env.NEXT_PUBLIC_BASE_URL}/books/${contentId}` - : `${process.env.NEXT_PUBLIC_BASE_URL}/#articles`; +// // 購入の確認 +// const hasPurchased = await checkUserPurchase(session.user.id!, contentId); +// if (!hasPurchased) { +// const redirectUrl = +// contentType === "book" +// ? `${process.env.NEXT_PUBLIC_BASE_URL}/books/${contentId}` +// : `${process.env.NEXT_PUBLIC_BASE_URL}/#articles`; - redirect(redirectUrl); - } -} +// redirect(redirectUrl); +// } +// } diff --git a/src/middleware.ts b/src/middleware.ts deleted file mode 100644 index 7af64f1..0000000 --- a/src/middleware.ts +++ /dev/null @@ -1 +0,0 @@ -export { auth as middleware } from "../auth";