관리자 기본 진입 경로 수정
This commit is contained in:
parent
778e8526d4
commit
bd046c4501
6 changed files with 62 additions and 25 deletions
|
|
@ -147,7 +147,18 @@ async function expectResponseOk(response: APIResponse | Response) {
|
|||
}
|
||||
}
|
||||
|
||||
async function mockApprovedAdminWithoutOnboarding(page: Page) {
|
||||
async function mockAdminSession(
|
||||
page: Page,
|
||||
authUser: Partial<{
|
||||
user_id: string;
|
||||
email: string;
|
||||
display_name: string;
|
||||
role: "learner" | "teacher" | "admin";
|
||||
admin_access: boolean;
|
||||
super_admin: boolean;
|
||||
onboarding_completed_at: number | null;
|
||||
}> = {},
|
||||
) {
|
||||
const seenAdminEndpoints = new Set<string>();
|
||||
const json = (body: unknown) => JSON.stringify(body);
|
||||
|
||||
|
|
@ -165,17 +176,17 @@ async function mockApprovedAdminWithoutOnboarding(page: Page) {
|
|||
|
||||
if (method === "GET" && path.endsWith("/auth/me")) {
|
||||
await fulfillJson({
|
||||
user_id: "stale-admin",
|
||||
email: "stale-admin@twentyoz.kr",
|
||||
display_name: "Stale Admin",
|
||||
role: "admin",
|
||||
admin_access: false,
|
||||
super_admin: false,
|
||||
user_id: authUser.user_id ?? "stale-admin",
|
||||
email: authUser.email ?? "stale-admin@twentyoz.kr",
|
||||
display_name: authUser.display_name ?? "Stale Admin",
|
||||
role: authUser.role ?? "admin",
|
||||
admin_access: authUser.admin_access ?? false,
|
||||
super_admin: authUser.super_admin ?? false,
|
||||
account_status: "approved",
|
||||
approval_required: false,
|
||||
cohort_ids: [],
|
||||
consent_at: null,
|
||||
onboarding_completed_at: null,
|
||||
onboarding_completed_at: authUser.onboarding_completed_at ?? null,
|
||||
nickname: "",
|
||||
self_introduction: "",
|
||||
avatar_url: "",
|
||||
|
|
@ -275,6 +286,10 @@ async function mockApprovedAdminWithoutOnboarding(page: Page) {
|
|||
return seenAdminEndpoints;
|
||||
}
|
||||
|
||||
async function mockApprovedAdminWithoutOnboarding(page: Page) {
|
||||
return mockAdminSession(page);
|
||||
}
|
||||
|
||||
async function signInAsAdmin(page: Page) {
|
||||
let res: APIResponse | null = null;
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
|
|
@ -593,6 +608,27 @@ test.describe("admin route guards", () => {
|
|||
.poll(() => Array.from(seenAdminEndpoints).sort())
|
||||
.toEqual(["health", "tickets", "uptime", "usage", "users"]);
|
||||
});
|
||||
|
||||
test("routes admin-entitled primary-role users from the root to the admin console", async ({ page }) => {
|
||||
const seenAdminEndpoints = await mockAdminSession(page, {
|
||||
user_id: "learner-admin",
|
||||
email: "learner-admin@twentyoz.kr",
|
||||
display_name: "Learner Admin",
|
||||
role: "learner",
|
||||
admin_access: true,
|
||||
super_admin: false,
|
||||
onboarding_completed_at: 1_782_900_000,
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page).toHaveURL(/\/admin$/);
|
||||
await expect(page.getByRole("heading", { name: "현재 서비스 상태" })).toBeVisible();
|
||||
await expect(page.locator(".vg-nav").getByRole("link", { name: "운영 홈" })).toBeVisible();
|
||||
await expect
|
||||
.poll(() => Array.from(seenAdminEndpoints).sort())
|
||||
.toEqual(["health", "tickets", "uptime", "usage", "users"]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("admin route", () => {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
import type { ReactNode } from "react";
|
||||
import { BrowserRouter, Navigate, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { AuthProvider, canAccessRole, useAuth, roleHomePath, type AuthUser, type Role } from "./lib/auth";
|
||||
import { AuthProvider, canAccessRole, initialPathForUser, useAuth, roleHomePath, type AuthUser, type Role } from "./lib/auth";
|
||||
|
||||
import Login from "./pages/Login";
|
||||
import Onboarding from "./pages/Onboarding";
|
||||
|
|
@ -79,10 +79,7 @@ function isAdminWorkspacePath(path: string) {
|
|||
}
|
||||
|
||||
function approvedHomePath(user: AuthUser) {
|
||||
if (user.onboardingCompletedAt == null) {
|
||||
return canAccessRole(user, "admin") ? "/admin" : "/onboarding";
|
||||
}
|
||||
return roleHomePath(user.role);
|
||||
return initialPathForUser(user);
|
||||
}
|
||||
|
||||
function OnboardingGate({ children }: { children: ReactNode }) {
|
||||
|
|
@ -99,7 +96,7 @@ function OnboardingGate({ children }: { children: ReactNode }) {
|
|||
return <Navigate to="/onboarding" replace state={{ from: path }} />;
|
||||
}
|
||||
if (user && user.onboardingCompletedAt != null && (path === "/login" || path === "/onboarding")) {
|
||||
return <Navigate to={roleHomePath(user.role)} replace />;
|
||||
return <Navigate to={initialPathForUser(user)} replace />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
@ -129,7 +126,7 @@ function RootRedirect() {
|
|||
if (user && user.onboardingCompletedAt == null) {
|
||||
return <Navigate to={approvedHomePath(user)} replace />;
|
||||
}
|
||||
return <Navigate to={user ? roleHomePath(user.role) : "/login"} replace />;
|
||||
return <Navigate to={user ? initialPathForUser(user) : "/login"} replace />;
|
||||
}
|
||||
|
||||
function AppRoutes() {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,14 @@ export function canAccessRole(user: AuthUser, role: Role): boolean {
|
|||
return role === "admin" && user.adminAccess;
|
||||
}
|
||||
|
||||
export function initialPathForUser(user: AuthUser): string {
|
||||
if (user.accountStatus !== "approved") return "/pending";
|
||||
if (user.onboardingCompletedAt == null) {
|
||||
return canAccessRole(user, "admin") ? "/admin" : "/onboarding";
|
||||
}
|
||||
return canAccessRole(user, "admin") ? "/admin" : roleHomePath(user.role);
|
||||
}
|
||||
|
||||
export function accessibleRolesFor(user: AuthUser): Role[] {
|
||||
if (user.superAdmin || user.role === "admin") return ["learner", "teacher", "admin"];
|
||||
if (user.adminAccess) return ["admin"];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { roleHomePath, useAuth, type Role } from "../lib/auth";
|
||||
import { initialPathForUser, useAuth, type Role } from "../lib/auth";
|
||||
import { apiUrl, authApi, type AuthConfigResponse } from "../lib/api";
|
||||
import { LoginBrand } from "./login/LoginBrand";
|
||||
import { LoginPanel, type LoginRoleOption } from "./login/LoginPanel";
|
||||
|
|
@ -172,14 +172,7 @@ export default function Login() {
|
|||
setLoginErrorReason(null);
|
||||
try {
|
||||
const signedIn = await login(role);
|
||||
navigate(
|
||||
signedIn.accountStatus !== "approved"
|
||||
? "/pending"
|
||||
: signedIn.onboardingCompletedAt == null
|
||||
? "/onboarding"
|
||||
: roleHomePath(signedIn.role),
|
||||
{ replace: true },
|
||||
);
|
||||
navigate(initialPathForUser(signedIn), { replace: true });
|
||||
} catch (err) {
|
||||
setLoginError(err instanceof Error ? err.message : "로그인에 실패했습니다.");
|
||||
} finally {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue