first round of review
This commit is contained in:
parent
870804e7b9
commit
23049da55a
15 changed files with 325 additions and 182 deletions
|
|
@ -43,7 +43,7 @@ export default function AdminInvitesPage() {
|
|||
const [createError, setCreateError] = useState<string | null>(null);
|
||||
const [users, setUsers] = useState<UserOption[]>([]);
|
||||
const { user, isLoading, isAuthorized } = useRequireAuth({
|
||||
requiredPermission: Permission.VIEW_AUDIT, // Admins have this
|
||||
requiredPermission: Permission.MANAGE_INVITES,
|
||||
fallbackRedirect: "/",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -43,37 +43,11 @@ export function Header({ currentPage }: HeaderProps) {
|
|||
|
||||
if (!user) return null;
|
||||
|
||||
// For admin pages, show admin navigation
|
||||
if (isAdminUser && (currentPage === "audit" || currentPage === "admin-invites")) {
|
||||
return (
|
||||
<div style={sharedStyles.header}>
|
||||
<div style={sharedStyles.nav}>
|
||||
{ADMIN_NAV_ITEMS.map((item, index) => (
|
||||
<span key={item.id}>
|
||||
{index > 0 && <span style={sharedStyles.navDivider}>•</span>}
|
||||
{item.id === currentPage ? (
|
||||
<span style={sharedStyles.navCurrent}>{item.label}</span>
|
||||
) : (
|
||||
<a href={item.href} style={sharedStyles.navLink}>
|
||||
{item.label}
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={sharedStyles.userInfo}>
|
||||
<span style={sharedStyles.userEmail}>{user.email}</span>
|
||||
<button onClick={handleLogout} style={sharedStyles.logoutBtn}>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For regular pages, build nav with links
|
||||
const visibleItems = REGULAR_NAV_ITEMS.filter(
|
||||
(item) => !item.regularOnly || isRegularUser
|
||||
// Build nav items based on user role
|
||||
// Admin users see admin nav items, regular users see regular nav items
|
||||
const navItems = isAdminUser ? ADMIN_NAV_ITEMS : REGULAR_NAV_ITEMS;
|
||||
const visibleItems = navItems.filter(
|
||||
(item) => (!item.regularOnly || isRegularUser) && (!item.adminOnly || isAdminUser)
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -258,9 +258,9 @@ describe("Home - Navigation", () => {
|
|||
|
||||
render(<Home />);
|
||||
|
||||
// Wait for render, then check profile link is not present
|
||||
// Wait for render - admin sees admin nav (Audit, Invites) not regular nav
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Counter")).toBeDefined();
|
||||
expect(screen.getByText("Audit")).toBeDefined();
|
||||
});
|
||||
expect(screen.queryByText("My Profile")).toBeNull();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,10 +7,13 @@ import { useAuth } from "../../auth-context";
|
|||
export default function SignupWithCodePage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { user, isLoading } = useAuth();
|
||||
const code = params.code as string;
|
||||
|
||||
useEffect(() => {
|
||||
// Wait for auth check to complete before redirecting
|
||||
if (isLoading) return;
|
||||
|
||||
if (user) {
|
||||
// Already logged in, redirect to home
|
||||
router.replace("/");
|
||||
|
|
@ -18,7 +21,7 @@ export default function SignupWithCodePage() {
|
|||
// Redirect to signup with code as query param
|
||||
router.replace(`/signup?code=${encodeURIComponent(code)}`);
|
||||
}
|
||||
}, [user, code, router]);
|
||||
}, [user, isLoading, code, router]);
|
||||
|
||||
return (
|
||||
<main style={{
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ import SignupPage from "./page";
|
|||
const mockPush = vi.fn();
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
useSearchParams: () => ({ get: () => null }),
|
||||
}));
|
||||
|
||||
vi.mock("../auth-context", () => ({
|
||||
useAuth: () => ({ register: vi.fn() }),
|
||||
useAuth: () => ({ user: null, register: vi.fn() }),
|
||||
}));
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
|
@ -16,19 +17,18 @@ afterEach(() => cleanup());
|
|||
|
||||
test("renders signup form with title", () => {
|
||||
render(<SignupPage />);
|
||||
expect(screen.getByRole("heading", { name: "Create account" })).toBeDefined();
|
||||
// Step 1 shows "Join with Invite" title (invite code entry)
|
||||
expect(screen.getByRole("heading", { name: "Join with Invite" })).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders email and password inputs", () => {
|
||||
test("renders invite code input", () => {
|
||||
render(<SignupPage />);
|
||||
expect(screen.getByLabelText("Email")).toBeDefined();
|
||||
expect(screen.getByLabelText("Password")).toBeDefined();
|
||||
expect(screen.getByLabelText("Confirm Password")).toBeDefined();
|
||||
expect(screen.getByLabelText("Invite Code")).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders create account button", () => {
|
||||
test("renders continue button", () => {
|
||||
render(<SignupPage />);
|
||||
expect(screen.getByRole("button", { name: "Create account" })).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Continue" })).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders link to login", () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, Suspense } from "react";
|
||||
import { useState, useEffect, useCallback, Suspense } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAuth } from "../auth-context";
|
||||
import { api } from "../api";
|
||||
|
|
@ -20,6 +20,7 @@ function SignupContent() {
|
|||
const [inviteValid, setInviteValid] = useState<boolean | null>(null);
|
||||
const [inviteError, setInviteError] = useState("");
|
||||
const [isCheckingInvite, setIsCheckingInvite] = useState(false);
|
||||
const [isCheckingInitialCode, setIsCheckingInitialCode] = useState(!!initialCode);
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
|
@ -37,14 +38,7 @@ function SignupContent() {
|
|||
}
|
||||
}, [user, router]);
|
||||
|
||||
// Check invite code on mount if provided in URL
|
||||
useEffect(() => {
|
||||
if (initialCode) {
|
||||
checkInvite(initialCode);
|
||||
}
|
||||
}, [initialCode]);
|
||||
|
||||
const checkInvite = async (code: string) => {
|
||||
const checkInvite = useCallback(async (code: string) => {
|
||||
if (!code.trim()) {
|
||||
setInviteValid(null);
|
||||
setInviteError("");
|
||||
|
|
@ -72,7 +66,14 @@ function SignupContent() {
|
|||
} finally {
|
||||
setIsCheckingInvite(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Check invite code on mount if provided in URL
|
||||
useEffect(() => {
|
||||
if (initialCode) {
|
||||
checkInvite(initialCode).finally(() => setIsCheckingInitialCode(false));
|
||||
}
|
||||
}, [initialCode, checkInvite]);
|
||||
|
||||
const handleInviteSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -110,6 +111,21 @@ function SignupContent() {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Show loading state while checking initial code from URL
|
||||
if (isCheckingInitialCode) {
|
||||
return (
|
||||
<main style={styles.main}>
|
||||
<div style={styles.container}>
|
||||
<div style={styles.card}>
|
||||
<div style={{ textAlign: "center", color: "rgba(255,255,255,0.6)" }}>
|
||||
Checking invite code...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// Step 1: Enter invite code
|
||||
if (!inviteValid) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -79,6 +79,53 @@ test.describe("Authentication Flow", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test.describe("Logged-in User Visiting Invite URL", () => {
|
||||
test("redirects to home when logged-in user visits direct invite URL", async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
// First sign up to create a user
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page.locator("h1")).toHaveText("Create account");
|
||||
|
||||
await page.fill('input#email', email);
|
||||
await page.fill('input#password', "password123");
|
||||
await page.fill('input#confirmPassword', "password123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL("/");
|
||||
|
||||
// Create another invite
|
||||
const anotherInvite = await createInvite(request);
|
||||
|
||||
// Visit invite URL while logged in - should redirect to home
|
||||
await page.goto(`/signup/${anotherInvite}`);
|
||||
await expect(page).toHaveURL("/");
|
||||
});
|
||||
|
||||
test("redirects to home when logged-in user visits signup page", async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
// Sign up and stay logged in
|
||||
await page.goto("/signup");
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page.locator("h1")).toHaveText("Create account");
|
||||
|
||||
await page.fill('input#email', email);
|
||||
await page.fill('input#password', "password123");
|
||||
await page.fill('input#confirmPassword', "password123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL("/");
|
||||
|
||||
// Try to visit signup page while logged in - should redirect to home
|
||||
await page.goto("/signup");
|
||||
await expect(page).toHaveURL("/");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Signup with Invite", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await clearAuth(page);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue