diff options
| -rw-r--r-- | backend/api/handler.go | 4 | ||||
| -rw-r--r-- | backend/api/handler_wrapper.go | 35 | ||||
| -rw-r--r-- | backend/gen/api/handler_wrapper_gen.go | 15 | ||||
| -rw-r--r-- | frontend/app/App.tsx | 16 | ||||
| -rw-r--r-- | frontend/app/pages/DashboardPage.tsx | 54 | ||||
| -rw-r--r-- | frontend/app/pages/GolfWatchPage.tsx | 18 | ||||
| -rw-r--r-- | frontend/app/pages/IndexPage.tsx | 3 |
7 files changed, 72 insertions, 73 deletions
diff --git a/backend/api/handler.go b/backend/api/handler.go index 57ae973..dcebfa1 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -218,7 +218,7 @@ func (h *Handler) GetGame(ctx context.Context, request GetGameRequestObject, use } return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - if !row.IsPublic && !user.IsAdmin { + if !row.IsPublic && (user == nil || !user.IsAdmin) { return GetGame404JSONResponse{ Message: "Game not found", }, nil @@ -318,7 +318,7 @@ func (h *Handler) GetGameWatchLatestStates(ctx context.Context, request GetGameW Status: status, } - if row.UserID == user.UserID && !user.IsAdmin { + if user != nil && row.UserID == user.UserID && !user.IsAdmin { return GetGameWatchLatestStates403JSONResponse{ Message: "You are one of the main players of this game", }, nil diff --git a/backend/api/handler_wrapper.go b/backend/api/handler_wrapper.go index 48a0eef..1c8bc83 100644 --- a/backend/api/handler_wrapper.go +++ b/backend/api/handler_wrapper.go @@ -28,12 +28,7 @@ func NewHandler(queries db.Querier, txm db.TxManager, hub GameHubInterface, auth } func (h *HandlerWrapper) GetGame(ctx context.Context, request GetGameRequestObject) (GetGameResponseObject, error) { - user, ok := GetUserFromContext(ctx) - if !ok { - return GetGame401JSONResponse{ - Message: "Unauthorized", - }, nil - } + user, _ := GetUserFromContext(ctx) return h.impl.GetGame(ctx, request, user) } @@ -58,32 +53,17 @@ func (h *HandlerWrapper) GetGamePlaySubmissions(ctx context.Context, request Get } func (h *HandlerWrapper) GetGameWatchLatestStates(ctx context.Context, request GetGameWatchLatestStatesRequestObject) (GetGameWatchLatestStatesResponseObject, error) { - user, ok := GetUserFromContext(ctx) - if !ok { - return GetGameWatchLatestStates401JSONResponse{ - Message: "Unauthorized", - }, nil - } + user, _ := GetUserFromContext(ctx) return h.impl.GetGameWatchLatestStates(ctx, request, user) } func (h *HandlerWrapper) GetGameWatchRanking(ctx context.Context, request GetGameWatchRankingRequestObject) (GetGameWatchRankingResponseObject, error) { - user, ok := GetUserFromContext(ctx) - if !ok { - return GetGameWatchRanking401JSONResponse{ - Message: "Unauthorized", - }, nil - } + user, _ := GetUserFromContext(ctx) return h.impl.GetGameWatchRanking(ctx, request, user) } func (h *HandlerWrapper) GetGames(ctx context.Context, request GetGamesRequestObject) (GetGamesResponseObject, error) { - user, ok := GetUserFromContext(ctx) - if !ok { - return GetGames401JSONResponse{ - Message: "Unauthorized", - }, nil - } + user, _ := GetUserFromContext(ctx) return h.impl.GetGames(ctx, request, user) } @@ -98,12 +78,7 @@ func (h *HandlerWrapper) GetMe(ctx context.Context, request GetMeRequestObject) } func (h *HandlerWrapper) GetTournament(ctx context.Context, request GetTournamentRequestObject) (GetTournamentResponseObject, error) { - user, ok := GetUserFromContext(ctx) - if !ok { - return GetTournament401JSONResponse{ - Message: "Unauthorized", - }, nil - } + user, _ := GetUserFromContext(ctx) return h.impl.GetTournament(ctx, request, user) } diff --git a/backend/gen/api/handler_wrapper_gen.go b/backend/gen/api/handler_wrapper_gen.go index e3e56c1..88f55c6 100644 --- a/backend/gen/api/handler_wrapper_gen.go +++ b/backend/gen/api/handler_wrapper_gen.go @@ -61,16 +61,26 @@ func main() { } slices.Sort(methods) + loginOptionalMethods := map[string]bool{ + "GetGames": true, + "GetGame": true, + "GetGameWatchLatestStates": true, + "GetGameWatchRanking": true, + "GetTournament": true, + } + type TemplateParameter struct { Name string RequiresLogin bool + LoginOptional bool RequiresAdminRole bool } templateParameters := make([]TemplateParameter, len(methods)) for i, method := range methods { templateParameters[i] = TemplateParameter{ Name: method, - RequiresLogin: method != "PostLogin", + RequiresLogin: method != "PostLogin" && !loginOptionalMethods[method], + LoginOptional: loginOptionalMethods[method], RequiresAdminRole: strings.Contains(method, "Admin"), } } @@ -144,6 +154,9 @@ func NewHandler(queries db.Querier, txm db.TxManager, hub GameHubInterface, auth } {{ end -}} return h.impl.{{ .Name }}(ctx, request, user) + {{ else if .LoginOptional -}} + user, _ := GetUserFromContext(ctx) + return h.impl.{{ .Name }}(ctx, request, user) {{ else -}} return h.impl.{{ .Name }}(ctx, request) {{ end -}} diff --git a/frontend/app/App.tsx b/frontend/app/App.tsx index 3f1333a..31adc28 100644 --- a/frontend/app/App.tsx +++ b/frontend/app/App.tsx @@ -26,9 +26,7 @@ export default function App() { </PublicOnlyRoute> </Route> <Route path="/dashboard"> - <ProtectedRoute> - <DashboardPage /> - </ProtectedRoute> + <DashboardPage /> </Route> <Route path="/golf/:gameId/preview"> {(params) => ( @@ -52,18 +50,10 @@ export default function App() { )} </Route> <Route path="/golf/:gameId/watch"> - {(params) => ( - <ProtectedRoute> - <GolfWatchPage gameId={params.gameId} /> - </ProtectedRoute> - )} + {(params) => <GolfWatchPage gameId={params.gameId} />} </Route> <Route path="/tournament/:tournamentId"> - {(params) => ( - <ProtectedRoute> - <TournamentPage tournamentId={params.tournamentId} /> - </ProtectedRoute> - )} + {(params) => <TournamentPage tournamentId={params.tournamentId} />} </Route> <Route> <div className="min-h-screen bg-gray-100 flex items-center justify-center"> diff --git a/frontend/app/pages/DashboardPage.tsx b/frontend/app/pages/DashboardPage.tsx index 00db3f0..92c0f04 100644 --- a/frontend/app/pages/DashboardPage.tsx +++ b/frontend/app/pages/DashboardPage.tsx @@ -14,7 +14,7 @@ type Game = components["schemas"]["Game"]; export default function DashboardPage() { usePageTitle(`Dashboard | ${APP_NAME}`); - const { user, logout } = useAuth(); + const { user, isLoggedIn, isLoading: authLoading, logout } = useAuth(); const [, navigate] = useLocation(); const [games, setGames] = useState<Game[]>([]); @@ -33,7 +33,7 @@ export default function DashboardPage() { navigate("/"); } - if (loading) { + if (loading || authLoading) { return ( <div className="min-h-screen bg-gray-100 flex items-center justify-center"> <p className="text-gray-500">Loading...</p> @@ -43,18 +43,24 @@ export default function DashboardPage() { return ( <div className="p-6 bg-gray-100 min-h-screen flex flex-col items-center gap-4"> - {user?.icon_path && ( + {isLoggedIn && user?.icon_path && ( <UserIcon iconPath={user.icon_path} displayName={user.display_name} className="w-24 h-24" /> )} - <h1 className="text-3xl font-bold text-gray-800">{user?.display_name}</h1> + {isLoggedIn ? ( + <h1 className="text-3xl font-bold text-gray-800"> + {user?.display_name} + </h1> + ) : ( + <h1 className="text-3xl font-bold text-gray-800">試合一覧</h1> + )} <BorderedContainerWithCaption caption="試合一覧"> <div className="px-4"> {games.length === 0 ? ( - <p>エントリーできる試合はありません</p> + <p>試合はありません</p> ) : ( <ul className="divide-y divide-gray-300"> {games.map((game) => ( @@ -68,20 +74,24 @@ export default function DashboardPage() { </span> </div> <div className="flex gap-2"> - {game.started_at == null && ( + {isLoggedIn && game.started_at == null && ( <NavigateLink to={`/golf/${game.game_id}/preview`}> 問題を見る </NavigateLink> )} - <NavigateLink to={`/golf/${game.game_id}/play`}> - 対戦 - </NavigateLink> + {isLoggedIn && ( + <NavigateLink to={`/golf/${game.game_id}/play`}> + 対戦 + </NavigateLink> + )} <NavigateLink to={`/golf/${game.game_id}/watch`}> 観戦 </NavigateLink> - <NavigateLink to={`/golf/${game.game_id}/submissions`}> - 提出履歴 - </NavigateLink> + {isLoggedIn && ( + <NavigateLink to={`/golf/${game.game_id}/submissions`}> + 提出履歴 + </NavigateLink> + )} </div> </li> ))} @@ -89,14 +99,18 @@ export default function DashboardPage() { )} </div> </BorderedContainerWithCaption> - <button - type="button" - onClick={handleLogout} - className="px-4 py-2 bg-red-500 text-white rounded-sm transition duration-300 hover:bg-red-700 focus:ring-3 focus:ring-red-400 focus:outline-hidden" - > - ログアウト - </button> - {user?.is_admin && ( + {isLoggedIn ? ( + <button + type="button" + onClick={handleLogout} + className="px-4 py-2 bg-red-500 text-white rounded-sm transition duration-300 hover:bg-red-700 focus:ring-3 focus:ring-red-400 focus:outline-hidden" + > + ログアウト + </button> + ) : ( + <NavigateLink to="/login">ログイン</NavigateLink> + )} + {isLoggedIn && user?.is_admin && ( <a href={ import.meta.env.DEV diff --git a/frontend/app/pages/GolfWatchPage.tsx b/frontend/app/pages/GolfWatchPage.tsx index 4f76136..168bd6f 100644 --- a/frontend/app/pages/GolfWatchPage.tsx +++ b/frontend/app/pages/GolfWatchPage.tsx @@ -1,6 +1,5 @@ import { createStore, Provider as JotaiProvider } from "jotai"; import { useEffect, useMemo, useState } from "react"; -import { useLocation } from "wouter"; import { ApiClientContext, createApiClient } from "../api/client"; import type { components } from "../api/schema"; import GolfWatchApp from "../components/GolfWatchApp"; @@ -12,14 +11,13 @@ type LatestGameState = components["schemas"]["LatestGameState"]; type RankingEntry = components["schemas"]["RankingEntry"]; export default function GolfWatchPage({ gameId }: { gameId: string }) { - const [, navigate] = useLocation(); - const [game, setGame] = useState<Game | null>(null); const [ranking, setRanking] = useState<RankingEntry[]>([]); const [gameStates, setGameStates] = useState<{ [key: string]: LatestGameState; }>({}); const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); const gameIdNum = Number(gameId); @@ -41,16 +39,16 @@ export default function GolfWatchPage({ gameId }: { gameId: string }) { setRanking(ranking); setGameStates(states); }) - .catch(() => navigate("/dashboard")) + .catch(() => setError(true)) .finally(() => setLoading(false)); - }, [gameIdNum, navigate]); + }, [gameIdNum]); const store = useMemo(() => { if (!game) return null; return createStore(); }, [game]); - if (loading || !game || !store) { + if (loading) { return ( <div className="min-h-screen bg-gray-100 flex items-center justify-center"> <p className="text-gray-500">Loading...</p> @@ -58,6 +56,14 @@ export default function GolfWatchPage({ gameId }: { gameId: string }) { ); } + if (error || !game || !store) { + return ( + <div className="min-h-screen bg-gray-100 flex items-center justify-center"> + <p className="text-red-500">試合が見つかりませんでした</p> + </div> + ); + } + return ( <JotaiProvider store={store}> <ApiClientContext.Provider value={createApiClient()}> diff --git a/frontend/app/pages/IndexPage.tsx b/frontend/app/pages/IndexPage.tsx index 120720a..03e135b 100644 --- a/frontend/app/pages/IndexPage.tsx +++ b/frontend/app/pages/IndexPage.tsx @@ -31,7 +31,8 @@ export default function IndexPage() { </p> </BorderedContainer> </div> - <div> + <div className="flex gap-4"> + <NavigateLink to="/dashboard">観戦する</NavigateLink> <NavigateLink to="/login">ログイン</NavigateLink> </div> </div> |
