auth: SSOログイン後の画面遷移とユーザー表示を修正
- コールバック成功時のリダイレクト先をWEB_ORIGINルートから /dashboardに変更(ランディングで止まらないように) - ルートページでログイン済みならダッシュボードへ自動遷移 - サイドバーのユーザーカードをOIDC identityの表示名・メールに変更し、 未ログイン時のみSSOログインリンクを表示 - demoヘッダ(x-demo-user)はdemoモード時のみ送信
This commit is contained in:
@@ -41,7 +41,9 @@ export async function api<T>(path: string, init: RequestInit = {}) {
|
||||
const headers = new Headers(init.headers);
|
||||
headers.set("accept", "application/json");
|
||||
if (init.body) headers.set("content-type", "application/json");
|
||||
if (AUTH_MODE === "demo") {
|
||||
Object.entries(DEMO_HEADERS).forEach(([key, value]) => headers.set(key, value));
|
||||
}
|
||||
const response = await fetch(`${API_BASE}${path}`, { ...init, headers, credentials: "include" });
|
||||
const body = (await response.json().catch(() => ({}))) as T & { error?: string; detail?: string };
|
||||
if (!response.ok) throw new Error(body.detail || body.error || `Request failed (${response.status})`);
|
||||
|
||||
+15
-1
@@ -1,10 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowRight, Boxes, FileArchive, Gauge, KeyRound, ShieldCheck, UploadCloud } from "lucide-react";
|
||||
import { AUTH_MODE, API_BASE } from "@/app/lib/dashboard";
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
|
||||
// SSOモードでログイン済みなら、ランディングを挟まずダッシュボードへ向かう。
|
||||
useEffect(() => {
|
||||
if (AUTH_MODE !== "oidc") return;
|
||||
let cancelled = false;
|
||||
fetch(`${API_BASE}/api/v1/me`, { credentials: "include" })
|
||||
.then((response) => { if (response.ok && !cancelled) router.replace("/dashboard"); })
|
||||
.catch(() => {});
|
||||
return () => { cancelled = true; };
|
||||
}, [router]);
|
||||
|
||||
return <main className="landing">
|
||||
<header className="landing-header">
|
||||
<Link className="landing-brand" href="/"><span className="admin-brand-mark">A</span><span>AltDock</span><span className="landing-beta">BETA</span></Link>
|
||||
@@ -24,7 +38,7 @@ export default function Home() {
|
||||
<Link className="ui-button ui-button-primary" href="/dashboard">ダッシュボードを開く <ArrowRight size={15} /></Link>
|
||||
<a className="ui-button ui-button-outline" href="https://faq.altstore.io/developers/distribute-with-altstore-pal" target="_blank" rel="noreferrer">配布要件を見る</a>
|
||||
</div>
|
||||
<div className="landing-hero-meta"><Gauge size={14} />Demo mode で起動中 · API <code>{API_BASE}</code></div>
|
||||
{AUTH_MODE === "demo" && <div className="landing-hero-meta"><Gauge size={14} />Demo mode で起動中 · API <code>{API_BASE}</code></div>}
|
||||
</div>
|
||||
<aside className="landing-hero-card" aria-hidden="true">
|
||||
<div className="landing-flow-step"><span className="landing-flow-num">1</span><span><UploadCloud size={16} />ADP ZIPをアップロード</span></div>
|
||||
|
||||
@@ -116,7 +116,8 @@ export async function registerAuthRoutes(app: FastifyInstance, appConfig: AppCon
|
||||
reply.clearCookie(stateCookie, { path: "/auth" });
|
||||
reply.clearCookie(verifierCookie, { path: "/auth" });
|
||||
reply.clearCookie(nonceCookie, { path: "/auth" });
|
||||
return reply.redirect(appConfig.WEB_ORIGIN);
|
||||
// SSO成功後はランディングではなく管理ダッシュボードへ直接向かう。
|
||||
return reply.redirect(`${appConfig.WEB_ORIGIN.replace(/\/$/, "")}/dashboard`);
|
||||
});
|
||||
|
||||
app.get("/auth/logout", async (_request, reply) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import Link from "next/link";
|
||||
import { useEffect, useMemo, useState, type ComponentType, type PropsWithChildren } from "react";
|
||||
import { AppWindow, BookOpen, Boxes, ChevronDown, ExternalLink, FileArchive, Gauge, Menu, PanelLeftClose, PanelLeftOpen, Plus, Search, Settings2, UploadCloud, X } from "lucide-react";
|
||||
import { API_BASE, AUTH_MODE, formatBytes } from "@/app/lib/dashboard";
|
||||
import type { Identity } from "@/packages/core/src/index";
|
||||
import { useDashboard } from "./dashboard-provider";
|
||||
|
||||
type NavItem = { href: string; label: string; icon: ComponentType<{ size?: number; strokeWidth?: number }> };
|
||||
@@ -34,6 +35,7 @@ function SideNavigation({ collapsed, onNavigate }: { collapsed?: boolean; onNavi
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { dashboard } = useDashboard();
|
||||
const { identity } = useIdentity();
|
||||
|
||||
const renderItem = (item: NavItem) => {
|
||||
const external = item.href.startsWith("http");
|
||||
@@ -55,10 +57,24 @@ function SideNavigation({ collapsed, onNavigate }: { collapsed?: boolean; onNavi
|
||||
<nav className="admin-nav admin-nav-secondary" aria-label="その他のナビゲーション">{secondaryNav.map(renderItem)}</nav>
|
||||
<div className="admin-sidebar-spacer" />
|
||||
{!collapsed && <div className="admin-storage-card"><div className="admin-storage-row"><span>保存容量</span><strong>{formatBytes(dashboard?.usage.storageBytes || 0)} / {formatBytes(dashboard?.limits.maxStorageBytes || 0)}</strong></div><div className="admin-progress"><span style={{ width: `${storagePercent}%` }} /></div><small>Free workspace plan</small></div>}
|
||||
{!collapsed && <div className="admin-user-card"><span className="admin-user-avatar">D</span><span><strong>Demo Developer</strong><small>demo@altdock.local</small></span><span className="admin-user-menu">···</span></div>}
|
||||
{!collapsed && <div className="admin-user-card"><span className="admin-user-avatar">{(identity?.displayName || "D").slice(0, 1).toUpperCase()}</span><span><strong>{identity?.displayName || "Demo Developer"}</strong><small>{identity?.email || "demo@altdock.local"}</small></span><span className="admin-user-menu">···</span></div>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function useIdentity() {
|
||||
const [identity, setIdentity] = useState<Identity | null>(null);
|
||||
useEffect(() => {
|
||||
if (AUTH_MODE !== "oidc") return;
|
||||
let cancelled = false;
|
||||
fetch(`${API_BASE}/api/v1/me`, { credentials: "include" })
|
||||
.then((response) => response.ok ? response.json() : null)
|
||||
.then((body) => { if (!cancelled && body?.identity) setIdentity(body.identity as Identity); })
|
||||
.catch(() => {});
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
return { identity };
|
||||
}
|
||||
|
||||
function CommandPalette({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
const router = useRouter();
|
||||
const commands = useMemo(() => primaryNav.map((item) => ({ ...item, action: () => router.push(item.href) })), [router]);
|
||||
@@ -71,6 +87,7 @@ export function AdminShell({ children }: PropsWithChildren) {
|
||||
const [commandOpen, setCommandOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const { notice, dismissNotice, dashboard } = useDashboard();
|
||||
const { identity } = useIdentity();
|
||||
|
||||
useEffect(() => {
|
||||
const handleShortcut = (event: KeyboardEvent) => {
|
||||
@@ -87,7 +104,7 @@ export function AdminShell({ children }: PropsWithChildren) {
|
||||
<aside className={`admin-sidebar ${collapsed ? "is-collapsed" : ""}`}><SideNavigation collapsed={collapsed} /></aside>
|
||||
<Dialog open={mobileOpen} onOpenChange={setMobileOpen}><DialogContent className="mobile-nav-dialog"><DialogTitle className="sr-only">管理画面ナビゲーション</DialogTitle><DialogDescription className="sr-only">管理画面の移動先を選択します。</DialogDescription><button className="mobile-nav-close" onClick={() => setMobileOpen(false)} aria-label="ナビゲーションを閉じる"><X size={17} /></button><SideNavigation onNavigate={() => setMobileOpen(false)} /></DialogContent></Dialog>
|
||||
<section className="admin-main-column">
|
||||
<header className="admin-topbar"><div className="admin-topbar-left"><Button className="mobile-menu-button" variant="ghost" size="icon" onClick={() => setMobileOpen(true)} aria-label="ナビゲーションを開く"><Menu size={18} /></Button><div className="admin-breadcrumb"><span>Workspace</span><span>/</span><strong>{pageLabel}</strong></div></div><div className="admin-topbar-actions"><button className="admin-search-trigger" onClick={() => setCommandOpen(true)}><Search size={15} /><span>検索</span><kbd>⌘ K</kbd></button><Link className="admin-topbar-action" href="/dashboard/releases?upload=1"><Plus size={15} />新しいリリース</Link>{AUTH_MODE === "oidc" ? <a className="admin-auth-link" href={`${API_BASE}/auth/login`}>SSOでログイン</a> : <a className="admin-auth-link" href={`${API_BASE}/healthz`} target="_blank" rel="noreferrer">API status</a>}</div></header>
|
||||
<header className="admin-topbar"><div className="admin-topbar-left"><Button className="mobile-menu-button" variant="ghost" size="icon" onClick={() => setMobileOpen(true)} aria-label="ナビゲーションを開く"><Menu size={18} /></Button><div className="admin-breadcrumb"><span>Workspace</span><span>/</span><strong>{pageLabel}</strong></div></div><div className="admin-topbar-actions"><button className="admin-search-trigger" onClick={() => setCommandOpen(true)}><Search size={15} /><span>検索</span><kbd>⌘ K</kbd></button><Link className="admin-topbar-action" href="/dashboard/releases?upload=1"><Plus size={15} />新しいリリース</Link>{AUTH_MODE === "oidc" ? (!identity ? <a className="admin-auth-link" href={`${API_BASE}/auth/login`}>SSOでログイン</a> : null) : <a className="admin-auth-link" href={`${API_BASE}/healthz`} target="_blank" rel="noreferrer">API status</a>}</div></header>
|
||||
<div className="admin-content">{children}</div>
|
||||
</section>
|
||||
<button className="admin-collapse-toggle" onClick={() => setCollapsed((value) => !value)} aria-label={collapsed ? "サイドバーを展開" : "サイドバーを折りたたむ"}>{collapsed ? <PanelLeftOpen size={16} /> : <PanelLeftClose size={16} />}</button>
|
||||
|
||||
Reference in New Issue
Block a user