Skip to content

Commit 1686d28

Browse files
authored
Merge pull request #123 from kc3hack/feature/issue-122-logout-button
PR #122: ログアウト機能のUI実装
2 parents a6557d3 + 8daadd2 commit 1686d28

File tree

3 files changed

+126
-41
lines changed

3 files changed

+126
-41
lines changed

backend/app/api/endpoints/auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ async def github_callback(
412412
path="/",
413413
httponly=True,
414414
secure=is_https,
415-
samesite="lax",
415+
samesite="none" if is_https else "lax",
416416
)
417417
return redirect
418418

@@ -431,7 +431,7 @@ def logout(response: Response) -> dict:
431431
path="/",
432432
httponly=True,
433433
secure=is_https,
434-
samesite="lax",
434+
samesite="none" if is_https else "lax",
435435
)
436436
return {"message": "ログアウトしました"}
437437

backend/tests/test_api/test_auth.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,51 @@ def test_logout_without_cookie_still_200(client):
338338
assert res.status_code == 200
339339

340340

341+
def test_logout_cookie_samesite_secure_for_https(client, monkeypatch):
342+
"""HTTPS環境でログアウト時、Set-Cookieにsamesite=none; secureが設定される(Issue #122)。
343+
344+
背景: Cookie削除は設定時と同じ属性(SameSite/Secure)が必須。
345+
本番環境(FRONTEND_URL=https://...)で確実に削除されることを担保する。
346+
"""
347+
# FRONTEND_URLをHTTPSに設定
348+
monkeypatch.setattr("app.core.config.settings.FRONTEND_URL", "https://example.com")
349+
350+
# まずCookieをセット(認証済みにする)
351+
state = _valid_state()
352+
client.cookies.set("oauth_state", state)
353+
mock_client = _mock_httpx_client()
354+
with patch("app.api.endpoints.auth.httpx.AsyncClient", return_value=mock_client):
355+
client.get(f"/api/v1/auth/github/callback?code=fake_code&state={state}")
356+
357+
# ログアウト
358+
res = client.post("/api/v1/auth/logout")
359+
assert res.status_code == 200
360+
361+
# Set-Cookieヘッダーを取得(access_token のみ削除される)
362+
set_cookie_headers = res.headers.get_list("set-cookie")
363+
assert len(set_cookie_headers) >= 1, "access_token の Cookie削除が必要"
364+
365+
# access_tokenのSet-Cookieヘッダーを特定
366+
access_token_header = next(
367+
(h for h in set_cookie_headers if "access_token" in h), None
368+
)
369+
assert (
370+
access_token_header is not None
371+
), "access_token の Cookie削除ヘッダーが見つからない"
372+
373+
# HTTPS環境では samesite=none と secure が設定されることを確認
374+
access_token_header_lower = access_token_header.lower()
375+
assert (
376+
"samesite=none" in access_token_header_lower
377+
), "HTTPS環境では samesite=none が必要(Cookie削除時も設定時と同じ属性が必須)"
378+
assert (
379+
"secure" in access_token_header_lower
380+
), "HTTPS環境では secure が必要(Cookie削除時も設定時と同じ属性が必須)"
381+
382+
# Cookieが削除されていることも確認(max-age=0)
383+
assert "max-age=0" in access_token_header_lower, "Cookie削除には max-age=0 が必要"
384+
385+
341386
# ---------------------------------------------------------------------------
342387
# GET /auth/github/callback - ネットワークエラー系 (T-1, T-2)
343388
# ---------------------------------------------------------------------------

frontend/src/features/dashboard/components/AppSidebar.tsx

Lines changed: 79 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
1-
'use client';
1+
"use client";
22

3-
import Link from 'next/link';
4-
import { usePathname } from 'next/navigation';
3+
import Link from "next/link";
4+
import { usePathname, useRouter } from "next/navigation";
5+
import { useState } from "react";
6+
import { logout } from "@/lib/api/auth";
57

68
export function AppSidebar() {
79
const pathname = usePathname();
10+
const router = useRouter();
11+
const [isLoggingOut, setIsLoggingOut] = useState(false);
12+
13+
const handleLogout = async () => {
14+
if (isLoggingOut) return;
15+
16+
setIsLoggingOut(true);
17+
try {
18+
await logout();
19+
router.push("/login");
20+
} catch (error) {
21+
alert(
22+
`ログアウトに失敗しました: ${error instanceof Error ? error.message : "不明なエラー"}`,
23+
);
24+
setIsLoggingOut(false);
25+
}
26+
};
827

928
const isActive = (path: string) => {
1029
return pathname === path || pathname?.startsWith(`${path}/`);
1130
};
1231

1332
const getLinkClass = (path: string) => {
14-
const baseClass = "flex items-center p-3 transition-all duration-200 group border-4 font-sans mb-3 rounded-none";
15-
33+
const baseClass =
34+
"flex items-center p-3 transition-all duration-200 group border-4 font-sans mb-3 rounded-none";
35+
1636
if (isActive(path)) {
1737
return `${baseClass} bg-[#FCD34D] border-black text-black shadow-[inset_4px_4px_0_rgba(0,0,0,0.2)] font-bold tracking-widest`;
1838
}
@@ -22,72 +42,92 @@ export function AppSidebar() {
2242
return (
2343
<aside className="fixed left-0 top-16 z-40 h-[calc(100vh-4rem)] w-64 -translate-x-full transition-transform sm:translate-x-0 bg-[#14532D] border-r-4 border-black">
2444
<div className="flex h-full flex-col overflow-y-auto px-4 py-8">
25-
2645
{/* Menu Title */}
2746
<div className="mb-8 px-2">
28-
<h2 className="text-xl font-bold tracking-widest text-[#4ADE80] font-sans border-b-4 border-[#4ADE80] pb-2 drop-shadow-[2px_2px_0_black]">
29-
MAIN MENU
30-
</h2>
47+
<h2 className="text-xl font-bold tracking-widest text-[#4ADE80] font-sans border-b-4 border-[#4ADE80] pb-2 drop-shadow-[2px_2px_0_black]">
48+
MAIN MENU
49+
</h2>
3150
</div>
3251

3352
{/* Navigation Items */}
3453
<ul className="space-y-4 font-bold">
3554
{/* Home */}
3655
<li>
37-
<Link
38-
href="/dashboard"
39-
className={getLinkClass('/dashboard')}
40-
>
41-
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">🏠</div>
56+
<Link href="/dashboard" className={getLinkClass("/dashboard")}>
57+
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">
58+
🏠
59+
</div>
4260
<span className="text-lg">HOME</span>
4361
</Link>
4462
</li>
45-
63+
4664
{/* Exercises */}
4765
<li>
48-
<Link
49-
href="/exercises"
50-
className={getLinkClass('/exercises')}
51-
>
52-
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">⚔️</div>
66+
<Link href="/exercises" className={getLinkClass("/exercises")}>
67+
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">
68+
⚔️
69+
</div>
5370
<span className="text-lg">QUESTS</span>
5471
</Link>
5572
</li>
56-
73+
5774
{/* Grades */}
5875
<li>
59-
<Link
60-
href="/grades"
61-
className={getLinkClass('/grades')}
62-
>
63-
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">📜</div>
76+
<Link href="/grades" className={getLinkClass("/grades")}>
77+
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">
78+
📜
79+
</div>
6480
<span className="text-lg">STATS</span>
6581
</Link>
6682
</li>
67-
83+
6884
{/* Rank Measurement */}
6985
<li>
7086
<Link
7187
href="/rank-measurement"
72-
className={getLinkClass('/rank-measurement')}
88+
className={getLinkClass("/rank-measurement")}
7389
>
74-
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">🎯</div>
90+
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">
91+
🎯
92+
</div>
7593
<span className="text-lg">RANK</span>
7694
</Link>
7795
</li>
7896
</ul>
79-
97+
98+
{/* Logout Button */}
99+
<div className="mt-auto mb-4">
100+
<button
101+
onClick={handleLogout}
102+
disabled={isLoggingOut}
103+
className={`
104+
w-full flex items-center p-3 transition-all duration-200
105+
border-4 font-sans rounded-none font-bold tracking-widest text-lg
106+
${
107+
isLoggingOut
108+
? "bg-gray-400 border-gray-600 text-gray-700 cursor-not-allowed"
109+
: "bg-[#EF4444] border-black text-white hover:bg-[#DC2626] shadow-[4px_4px_0_black] active:shadow-none active:translate-x-1 active:translate-y-1"
110+
}
111+
`}
112+
>
113+
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">
114+
{isLoggingOut ? "⏳" : "🚪"}
115+
</div>
116+
<span>{isLoggingOut ? "LOGGING OUT..." : "LOGOUT"}</span>
117+
</button>
118+
</div>
119+
80120
{/* System Info Box */}
81121
<div className="mt-auto border-4 border-black bg-black p-4 text-[#4ADE80] font-sans text-xs tracking-wider">
82-
<p className="mb-2 border-b border-[#4ADE80] pb-1">SYSTEM STATUS</p>
83-
<div className="flex justify-between mb-1">
84-
<span>ONLINE</span>
85-
<span className="animate-pulse text-[#FCD34D]"></span>
86-
</div>
87-
<div className="flex justify-between">
88-
<span>VER.</span>
89-
<span>2.0.26</span>
90-
</div>
122+
<p className="mb-2 border-b border-[#4ADE80] pb-1">SYSTEM STATUS</p>
123+
<div className="flex justify-between mb-1">
124+
<span>ONLINE</span>
125+
<span className="animate-pulse text-[#FCD34D]"></span>
126+
</div>
127+
<div className="flex justify-between">
128+
<span>VER.</span>
129+
<span>2.0.26</span>
130+
</div>
91131
</div>
92132
</div>
93133
</aside>

0 commit comments

Comments
 (0)