From cfaf09e07efe230e5c9613d0906879e30d9c8b93 Mon Sep 17 00:00:00 2001 From: "Yuito Akatsuki (Tani Yutaka)" Date: Tue, 18 Feb 2025 19:05:16 +0900 Subject: [PATCH 1/2] Improve club data retrieval and API authentication handling (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :recycle: クラブ一覧のデータ取得を改善し、非同期処理を統合 * API認証処理を改善し、セッションがない場合にAPIキーからメールアドレスを復号化してセッションを設定 * :recycle: クラブ検索の非同期処理を改善 * デフォルト値の設定 * クリーンアップ * ワークフローのブランチ設定を追加し、sandboxおよびdevブランチでのプルリクエストとプッシュをサポート --- .github/workflows/VercelPreview.yaml | 2 + .github/workflows/lint.yaml | 4 ++ src/app/api/clubs/recent/route.ts | 2 - src/app/api/clubs/search/route.ts | 14 ++++- src/app/clubs/page.tsx | 42 ++++++++++++- src/app/search/page.tsx | 48 +++++++++++++-- src/components/ClubList.tsx | 71 ++++++---------------- src/components/search/SearchBox.tsx | 6 +- src/components/search/SearchView.tsx | 89 +++++++++++----------------- src/lib/client/club.ts | 14 ----- 10 files changed, 157 insertions(+), 135 deletions(-) delete mode 100644 src/lib/client/club.ts diff --git a/.github/workflows/VercelPreview.yaml b/.github/workflows/VercelPreview.yaml index a98fd15..1823776 100644 --- a/.github/workflows/VercelPreview.yaml +++ b/.github/workflows/VercelPreview.yaml @@ -3,6 +3,8 @@ on: pull_request: branches: - main + - sandbox + - dev jobs: checkLabels: runs-on: ubuntu-latest diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 44dd68f..e37f840 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -6,9 +6,13 @@ on: push: branches: - main + - sandbox + - dev pull_request: branches: - main + - sandbox + - dev jobs: lint: runs-on: ubuntu-latest diff --git a/src/app/api/clubs/recent/route.ts b/src/app/api/clubs/recent/route.ts index aad2488..22c7d98 100644 --- a/src/app/api/clubs/recent/route.ts +++ b/src/app/api/clubs/recent/route.ts @@ -10,11 +10,9 @@ const endpoint = process.env.DB_API_ENDPOINT; export const GET = async () => { const session = await auth(); const apiKey = (await headers()).get("X-Api-Key") as string; - console.log(`RT: ${apiKey}`); const email = crypto.AES.decrypt(apiKey, process.env.API_ROUTE_SECRET as string).toString( crypto.enc.Utf8 ); - console.log(`RT: ${email}`); const apiCheck = email && (email.endsWith("@nnn.ed.jp") || email.endsWith("@nnn.ac.jp") || email.endsWith("@n-jr.jp")); diff --git a/src/app/api/clubs/search/route.ts b/src/app/api/clubs/search/route.ts index 73dfba6..0ea000b 100644 --- a/src/app/api/clubs/search/route.ts +++ b/src/app/api/clubs/search/route.ts @@ -2,13 +2,25 @@ import { auth } from "@/auth"; import Club from "@/models/Club"; import { NextRequest, NextResponse } from "next/server"; +import crypto from "crypto-js"; +import { Session } from "next-auth"; const endpoint = process.env.DB_API_ENDPOINT; export const GET = async (req: NextRequest) => { const searchParams = req.nextUrl.searchParams; const query = searchParams.get("query"); - const session = await auth(); + let session: boolean | Session | null = await auth(); + if (!session) { + const headers = req.headers; + const apiKey = headers.get("X-Api-Key"); + const email = crypto.AES.decrypt( + apiKey as string, + process.env.API_ROUTE_SECRET as string + ).toString(crypto.enc.Utf8); + if (email.endsWith("@nnn.ed.jp") || email.endsWith("@nnn.ac.jp") || email.endsWith("@n-jr.jp")) + session = true; + } const response = await fetch( `${endpoint}/clubs?${query ? `&search=${query}` : ""}&order=created_at,desc` ); diff --git a/src/app/clubs/page.tsx b/src/app/clubs/page.tsx index ad596fa..be36228 100644 --- a/src/app/clubs/page.tsx +++ b/src/app/clubs/page.tsx @@ -1,7 +1,12 @@ +import { auth } from "@/auth"; import ClubList from "@/components/ClubList"; import ClubSearchForm from "@/components/search/SearchBox"; -import { Box, Stack, Typography } from "@mui/material"; +import Club from "@/models/Club"; +import { Box, CircularProgress, Stack, Typography } from "@mui/material"; import { Metadata } from "next"; +import { headers } from "next/headers"; +import { Suspense } from "react"; +import CryptoJS from "crypto-js"; export const metadata: Metadata = { title: "同好会一覧 - Linkle", @@ -9,6 +14,37 @@ export const metadata: Metadata = { }; export default async function Home() { + const headersData = await headers(); + const host = headersData.get("host"); + const protocol = + headersData.get("x-forwarded-proto") ?? host?.startsWith("localhost") ? "http" : "https"; + const cookie = headersData.get("cookie"); + const sessionID = cookie?.split(";").find((c) => c.trim().startsWith("authjs.session-token")); + const apiBase = `${protocol}://${host}`; + + const fetchData = new Promise(async (resolve, reject) => { + try { + const session = await auth(); + const res = await fetch(`${apiBase}/api/clubs/search`, { + headers: { + "Content-Type": "application/json", + "X-Api-key": CryptoJS.AES.encrypt( + session?.user?.email ?? "No Auth", + process.env.API_ROUTE_SECRET as string + ).toString(), + ...(sessionID && { Cookie: sessionID }), + }, + }); + if (!res.ok) { + throw new Error("Failed to fetch data" + res.statusText); + } + resolve((await res.json()) as Club[]); + } catch (error) { + console.log(error); + reject("Failed to fetch data" + error); + } + }); + return ( - + }> + + ); } diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index b79e053..bcc8999 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -1,15 +1,50 @@ +import { auth } from "@/auth"; import ClubSearchForm from "@/components/search/SearchBox"; import SearchResultsPage from "@/components/search/SearchView"; import SearchTitle from "@/components/search/SerachTitle"; -import { Box, Stack } from "@mui/material"; +import Club from "@/models/Club"; +import { Box, CircularProgress, Stack } from "@mui/material"; import { Metadata } from "next"; +import { headers } from "next/headers"; +import { Suspense } from "react"; +import CryptoJS from "crypto-js"; export const metadata: Metadata = { title: "クラブ検索 - Linkle", description: "Linkleのクラブ検索ページです。", }; -export default function Home() { +export default async function Home({ searchParams }: { searchParams: Promise<{ query: string }> }) { + const { query } = await searchParams; + const headersData = await headers(); + const host = headersData.get("host"); + const protocol = + headersData.get("x-forwarded-proto") ?? host?.startsWith("localhost") ? "http" : "https"; + const cookie = headersData.get("cookie"); + const sessionID = cookie?.split(";").find((c) => c.trim().startsWith("authjs.session-token")); + const apiBase = `${protocol}://${host}`; + const fetchData = new Promise(async (resolve, reject) => { + if (query) { + const session = await auth(); + const res = await fetch(`${apiBase}/api/clubs/search?query=${query}`, { + headers: { + "Content-Type": "application/json", + "X-Api-key": CryptoJS.AES.encrypt( + session?.user?.email ?? "No Auth", + process.env.API_ROUTE_SECRET as string + ).toString(), + ...(sessionID && { Cookie: sessionID }), + }, + }); + if (res.ok) { + const data = await res.json(); + resolve(data); + } else { + const error = await res.text(); + reject(error); + } + } else resolve("queryが指定されていません。"); + }); return ( <> - + - + }> + + ); diff --git a/src/components/ClubList.tsx b/src/components/ClubList.tsx index a021eac..d593ce4 100644 --- a/src/components/ClubList.tsx +++ b/src/components/ClubList.tsx @@ -1,45 +1,32 @@ "use client"; -import React, { Suspense } from "react"; -import { - Typography, - CircularProgress, - Alert, - Grid2, - Pagination, - PaginationItem, - Stack, -} from "@mui/material"; +import React, { use } from "react"; +import { Typography, Alert, Grid2, Pagination, PaginationItem, Stack } from "@mui/material"; import { useSearchParams } from "next/navigation"; import Link from "next/link"; import ClubCard from "./ClubCard"; import Club from "@/models/Club"; -const SearchResultsPage: React.FC = () => { +const ClubList = ({ fetchData }: { fetchData: Promise }) => { const searchParams = useSearchParams(); - const query = searchParams.get("query") || null; const page = searchParams.get("page"); - const [clubs, setSearchResult] = React.useState(null); - const [searchError, setSearchError] = React.useState(null); - const [loading, setLoading] = React.useState(false); + const clubs = use(fetchData); + + if (typeof clubs === "string") { + return ( + + {clubs} + + ); + } - React.useEffect(() => { - const fetchData = async () => { - setLoading(true); - setSearchError(null); - try { - const result = await (await fetch(`/api/clubs/search`)).json(); - setSearchResult(result); - } catch (error) { - setSearchError("検索中にエラーが発生しました。もう一度お試しください。"); - console.log(error); - } finally { - setLoading(false); - } - }; - fetchData(); - }, [query]); return ( { justifyContent="center" width={"100%"} > - {searchError && ( - - - {searchError} - - - )} - {loading && ( - - - - )} - {clubs && clubs.length > 0 && ( <> {clubs.map((club, index) => { @@ -129,10 +100,4 @@ const SearchResultsPage: React.FC = () => { ); }; -const ClubList = () => ( - }> - - -); - export default ClubList; diff --git a/src/components/search/SearchBox.tsx b/src/components/search/SearchBox.tsx index 2168b46..f374afc 100644 --- a/src/components/search/SearchBox.tsx +++ b/src/components/search/SearchBox.tsx @@ -7,9 +7,9 @@ import { useRouter } from "next/navigation"; // Next.js のルーターを使用 import theme from "@/theme/primary"; import formTheme from "@/theme/form"; -const ClubSearchForm: React.FC = () => { +const ClubSearchForm = ({ query }: { query?: string | undefined }) => { const { control, handleSubmit } = useForm<{ query: string }>({ - defaultValues: { query: "" }, + defaultValues: { query: query }, }); const router = useRouter(); @@ -35,10 +35,10 @@ const ClubSearchForm: React.FC = () => { render={({ field }) => ( )} /> diff --git a/src/components/search/SearchView.tsx b/src/components/search/SearchView.tsx index 690ac56..9ee01b1 100644 --- a/src/components/search/SearchView.tsx +++ b/src/components/search/SearchView.tsx @@ -1,47 +1,46 @@ "use client"; -import React, { Suspense } from "react"; -import { - Typography, - CircularProgress, - Alert, - Grid2, - Pagination, - PaginationItem, - Stack, -} from "@mui/material"; +import React, { use } from "react"; +import { Typography, Alert, Grid2, Pagination, PaginationItem, Stack } from "@mui/material"; import { useSearchParams } from "next/navigation"; import Link from "next/link"; import ClubCard from "../ClubCard"; import Club from "@/models/Club"; -const SearchResultsPage: React.FC = () => { +const SearchResultsPage = ({ + promise, + query, +}: { + promise: Promise; + query: string; +}) => { const searchParams = useSearchParams(); - const query = searchParams.get("query") || null; const page = searchParams.get("page"); - const [clubs, setSearchResult] = React.useState(null); - const [searchError, setSearchError] = React.useState(null); - const [loading, setLoading] = React.useState(false); + const clubs = use(promise); + + if (clubs instanceof Error) { + return ( + + {clubs.message} + + ); + } + + if (typeof clubs === "string") { + return ( + + {clubs} + + ); + } - React.useEffect(() => { - const fetchData = async () => { - if (query) { - setLoading(true); - setSearchError(null); - try { - const result = await (await fetch(`/api/clubs/search?query=${query}`)).json(); - setSearchResult(result); - } catch (error) { - setSearchError("検索中にエラーが発生しました。もう一度お試しください。"); - console.log(error); - } finally { - setLoading(false); - } - } else setSearchError("queryが指定されていません。"); - }; - fetchData(); - }, [query]); return ( { justifyContent="center" width={"100%"} > - {searchError && ( - - - {searchError} - - - )} - {loading && ( - - - - )} - {clubs && clubs.length > 0 && ( <> {clubs.map((club, index) => { @@ -131,10 +114,4 @@ const SearchResultsPage: React.FC = () => { ); }; -const SearchResultsPageWrapper = () => ( - }> - - -); - -export default SearchResultsPageWrapper; +export default SearchResultsPage; diff --git a/src/lib/client/club.ts b/src/lib/client/club.ts deleted file mode 100644 index a477c66..0000000 --- a/src/lib/client/club.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Club from "@/models/Club"; -import { fetchErrorResponse } from "../server/club"; - -export const getClubByIdClient = async (id: string): Promise => { - try { - const res = await fetch(`/api/clubs/${id}`); - if (res.status == 403) return "forbidden"; - const club = (await res.json()) as Club; - if (!club) return "notfound"; - return club; - } catch (e) { - throw new Error(e as string); - } -}; From 9d95b816074d13f3ae5ac05901579fb4a2f30990 Mon Sep 17 00:00:00 2001 From: "Yuito Akatsuki (Tani Yutaka)" Date: Tue, 18 Feb 2025 19:17:38 +0900 Subject: [PATCH 2/2] =?UTF-8?q?GitHub=20Actions=E3=82=92=E6=B4=BB=E7=94=A8?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=82=BF=E3=82=B0=E3=83=AF=E3=83=BC=E3=82=AF?= =?UTF-8?q?=E3=83=95=E3=83=AD=E3=83=BC=E3=82=92=E8=BF=BD=E5=8A=A0=20(#75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/Tagger.yaml | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/Tagger.yaml diff --git a/.github/workflows/Tagger.yaml b/.github/workflows/Tagger.yaml new file mode 100644 index 0000000..b872cec --- /dev/null +++ b/.github/workflows/Tagger.yaml @@ -0,0 +1,46 @@ +name: npm versioner + +on: + workflow_dispatch: + inputs: + version: + description: "Version to bump to" + required: true + default: "patch" + type: choice + options: + - "major" + - "minor" + - "patch" + prefix: + description: "Prefix for the pre-version" + required: false + type: choice + options: + - "alpha" + - "rc" + +permissions: + id-token: write + contents: write + +jobs: + version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Bump version(Normal) + if: ${{ github.event.inputs.prefix == null }} + run: npm version ${{ github.event.inputs.version }} + - name: Bump version(Pre-release) + if: ${{ github.event.inputs.prefix != null }} + run: npm version pre${{ github.event.inputs.version }} --preid=${{ github.event.inputs.prefix }} + - run: | + git config user.name "actions-user" + git config user.email "action@github.com" + - name: Push changes + run: | + git push --follow-tags + git push + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}