Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/app/api/endpoints/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ async def github_callback(
path="/",
httponly=True,
secure=is_https,
samesite="lax",
samesite="none" if is_https else "lax",
)
return redirect

Expand All @@ -431,7 +431,7 @@ def logout(response: Response) -> dict:
path="/",
httponly=True,
secure=is_https,
samesite="lax",
samesite="none" if is_https else "lax",
)
Comment on lines 431 to 435
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ログアウトの delete_cookiesamesite="none" if is_https else "lax" に変更されていますが、既存テストはCookieが消えること(max-age=0等)しか見ておらず、今回の修正目的である「属性一致(SameSite/Secure)により本番で確実に削除できる」ことを担保できません。FRONTEND_URLをhttpsにした場合のSet-Cookieヘッダーに samesite=nonesecure が出ることを追加でアサートしてください。

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

return {"message": "ログアウトしました"}

Expand Down
45 changes: 45 additions & 0 deletions backend/tests/test_api/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,51 @@ def test_logout_without_cookie_still_200(client):
assert res.status_code == 200


def test_logout_cookie_samesite_secure_for_https(client, monkeypatch):
"""HTTPS環境でログアウト時、Set-Cookieにsamesite=none; secureが設定される(Issue #122)。

背景: Cookie削除は設定時と同じ属性(SameSite/Secure)が必須。
本番環境(FRONTEND_URL=https://...)で確実に削除されることを担保する。
"""
# FRONTEND_URLをHTTPSに設定
monkeypatch.setattr("app.core.config.settings.FRONTEND_URL", "https://example.com")

# まずCookieをセット(認証済みにする)
state = _valid_state()
client.cookies.set("oauth_state", state)
mock_client = _mock_httpx_client()
with patch("app.api.endpoints.auth.httpx.AsyncClient", return_value=mock_client):
client.get(f"/api/v1/auth/github/callback?code=fake_code&state={state}")

# ログアウト
res = client.post("/api/v1/auth/logout")
assert res.status_code == 200

# Set-Cookieヘッダーを取得(access_token のみ削除される)
set_cookie_headers = res.headers.get_list("set-cookie")
assert len(set_cookie_headers) >= 1, "access_token の Cookie削除が必要"

# access_tokenのSet-Cookieヘッダーを特定
access_token_header = next(
(h for h in set_cookie_headers if "access_token" in h), None
)
assert (
access_token_header is not None
), "access_token の Cookie削除ヘッダーが見つからない"

# HTTPS環境では samesite=none と secure が設定されることを確認
access_token_header_lower = access_token_header.lower()
assert (
"samesite=none" in access_token_header_lower
), "HTTPS環境では samesite=none が必要(Cookie削除時も設定時と同じ属性が必須)"
assert (
"secure" in access_token_header_lower
), "HTTPS環境では secure が必要(Cookie削除時も設定時と同じ属性が必須)"

# Cookieが削除されていることも確認(max-age=0)
assert "max-age=0" in access_token_header_lower, "Cookie削除には max-age=0 が必要"


# ---------------------------------------------------------------------------
# GET /auth/github/callback - ネットワークエラー系 (T-1, T-2)
# ---------------------------------------------------------------------------
Expand Down
118 changes: 79 additions & 39 deletions frontend/src/features/dashboard/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
'use client';
"use client";

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { logout } from "@/lib/api/auth";

export function AppSidebar() {
const pathname = usePathname();
const router = useRouter();
const [isLoggingOut, setIsLoggingOut] = useState(false);

const handleLogout = async () => {
if (isLoggingOut) return;

setIsLoggingOut(true);
try {
await logout();
router.push("/login");
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ログアウト後の遷移が router.push("/login") だと履歴にダッシュボードが残るため、戻る操作で保護ページに戻れてしまいます(withAuth 側で弾かれるとしてもUXが悪化しやすいです)。同リポジトリ内の認証リダイレクトは router.replace("/login") を使っているので、ここも replace に揃えるのが安全です。

Suggested change
router.push("/login");
router.replace("/login");

Copilot uses AI. Check for mistakes.
} catch (error) {
alert(
`ログアウトに失敗しました: ${error instanceof Error ? error.message : "不明なエラー"}`,
);
setIsLoggingOut(false);
}
};

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

const getLinkClass = (path: string) => {
const baseClass = "flex items-center p-3 transition-all duration-200 group border-4 font-sans mb-3 rounded-none";

const baseClass =
"flex items-center p-3 transition-all duration-200 group border-4 font-sans mb-3 rounded-none";

if (isActive(path)) {
return `${baseClass} bg-[#FCD34D] border-black text-black shadow-[inset_4px_4px_0_rgba(0,0,0,0.2)] font-bold tracking-widest`;
}
Expand All @@ -22,72 +42,92 @@ export function AppSidebar() {
return (
<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">
<div className="flex h-full flex-col overflow-y-auto px-4 py-8">

{/* Menu Title */}
<div className="mb-8 px-2">
<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]">
MAIN MENU
</h2>
<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]">
MAIN MENU
</h2>
</div>

{/* Navigation Items */}
<ul className="space-y-4 font-bold">
{/* Home */}
<li>
<Link
href="/dashboard"
className={getLinkClass('/dashboard')}
>
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">🏠</div>
<Link href="/dashboard" className={getLinkClass("/dashboard")}>
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">
🏠
</div>
<span className="text-lg">HOME</span>
</Link>
</li>

{/* Exercises */}
<li>
<Link
href="/exercises"
className={getLinkClass('/exercises')}
>
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">⚔️</div>
<Link href="/exercises" className={getLinkClass("/exercises")}>
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">
⚔️
</div>
<span className="text-lg">QUESTS</span>
</Link>
</li>

{/* Grades */}
<li>
<Link
href="/grades"
className={getLinkClass('/grades')}
>
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">📜</div>
<Link href="/grades" className={getLinkClass("/grades")}>
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">
📜
</div>
<span className="text-lg">STATS</span>
</Link>
</li>

{/* Rank Measurement */}
<li>
<Link
href="/rank-measurement"
className={getLinkClass('/rank-measurement')}
className={getLinkClass("/rank-measurement")}
>
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">🎯</div>
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">
🎯
</div>
<span className="text-lg">RANK</span>
</Link>
</li>
</ul>


{/* Logout Button */}
<div className="mt-auto mb-4">
<button
onClick={handleLogout}
disabled={isLoggingOut}
className={`
Comment on lines +99 to +103
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logout Button のラッパーに mt-auto を付けたことで、下の System Info Box 側にも mt-auto が残っている場合、flexの自動マージンが分散して「ログアウトボタンがSYSTEM STATUSの直上に来ない/大きな余白が入る」レイアウト崩れが起きます。どちらか片方だけに mt-auto を付けるか、2つをまとめた下部コンテナ1つに mt-auto を付けてその中で縦並びにしてください。

Copilot uses AI. Check for mistakes.
w-full flex items-center p-3 transition-all duration-200
border-4 font-sans rounded-none font-bold tracking-widest text-lg
${
isLoggingOut
? "bg-gray-400 border-gray-600 text-gray-700 cursor-not-allowed"
: "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"
}
`}
>
<div className="flex h-8 w-8 items-center justify-center text-xl mr-3 bg-white border-2 border-black text-black">
{isLoggingOut ? "⏳" : "🚪"}
</div>
<span>{isLoggingOut ? "LOGGING OUT..." : "LOGOUT"}</span>
</button>
</div>

{/* System Info Box */}
<div className="mt-auto border-4 border-black bg-black p-4 text-[#4ADE80] font-sans text-xs tracking-wider">
<p className="mb-2 border-b border-[#4ADE80] pb-1">SYSTEM STATUS</p>
<div className="flex justify-between mb-1">
<span>ONLINE</span>
<span className="animate-pulse text-[#FCD34D]">●</span>
</div>
<div className="flex justify-between">
<span>VER.</span>
<span>2.0.26</span>
</div>
<p className="mb-2 border-b border-[#4ADE80] pb-1">SYSTEM STATUS</p>
<div className="flex justify-between mb-1">
<span>ONLINE</span>
<span className="animate-pulse text-[#FCD34D]">●</span>
</div>
<div className="flex justify-between">
<span>VER.</span>
<span>2.0.26</span>
</div>
</div>
</div>
</aside>
Expand Down
Loading