Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
dc269b6
Install solid/reactive-authentication package
PreciousOritsedere Jul 29, 2026
e94e4cb
feat: add OAuth authorization code callback page
PreciousOritsedere Jul 29, 2026
dc8d2ac
chore: add fetch instrumentation for auth spike
PreciousOritsedere Jul 29, 2026
9dd4820
chore: add reactive-auth spike harness
PreciousOritsedere Jul 29, 2026
de8739a
chore: add reactive-auth spike experiments
PreciousOritsedere Jul 29, 2026
8914c8b
chore: update reactive-auth package to point to github branch(temp)
PreciousOritsedere Jul 29, 2026
d0e4809
chore: delete temporary spike custom-element
PreciousOritsedere Jul 29, 2026
1824861
chore: move custom element types to app/lib/auth
PreciousOritsedere Jul 29, 2026
064bf44
chore: move elements.ts to lib folder
PreciousOritsedere Jul 29, 2026
7d12b5f
chore: simplify element code
PreciousOritsedere Jul 29, 2026
2b95e78
feat: add reactive-auth manager
PreciousOritsedere Jul 29, 2026
cdc71d6
chore: update comment
PreciousOritsedere Jul 29, 2026
bcb0e45
chore: point spike harness at the shared auth core
PreciousOritsedere Jul 29, 2026
b38e22c
feat: add SolidAuthProvider
PreciousOritsedere Jul 29, 2026
b639992
feat: add useSolidAuth hook
PreciousOritsedere Jul 29, 2026
948c0c9
feat: mount SolidAuthProvider in the root layout
PreciousOritsedere Jul 29, 2026
f4fbbb0
chore: exercise SolidAuthProvider from the spike harness
PreciousOritsedere Jul 29, 2026
19b88fa
refactor: hold the current session in the auth manager
PreciousOritsedere Jul 29, 2026
3c608fe
feat: expose oidcIssuer on the profile agent
PreciousOritsedere Jul 29, 2026
e32b1e0
feat: identify a WebID or login server from one input
PreciousOritsedere Jul 29, 2026
5973074
feat: rebuild the login page on WebID entry
PreciousOritsedere Jul 29, 2026
1f9e497
chore: drop usage of solid-react-component
PreciousOritsedere Jul 29, 2026
f985d82
chore: drop ldo and solid-react component usage
PreciousOritsedere Jul 29, 2026
dd622bd
feat: guard routes with the app's own auth state
PreciousOritsedere Jul 29, 2026
d8f7174
fix: keep the remembered entry out of the suggestion list
PreciousOritsedere Jul 29, 2026
e22f4d9
perf: check the profile before asking whether a URL is an issuer
PreciousOritsedere Jul 29, 2026
c5204d0
feat: read auth state from the app's own provider
PreciousOritsedere Jul 29, 2026
d03b5a4
refactor: route data calls through the reactive fetch
PreciousOritsedere Jul 30, 2026
b683d1c
chore: drop ldo and solid-react component usage
PreciousOritsedere Jul 30, 2026
6430bff
fix: sign in before leaving the login page
PreciousOritsedere Jul 30, 2026
cc3f428
perf: attach credentials before the first request
PreciousOritsedere Jul 30, 2026
3c848b9
fix: sync the browse URL without a route navigation
PreciousOritsedere Jul 30, 2026
1cc266b
fix: give each auth redirect a single owner
PreciousOritsedere Jul 30, 2026
c3da6f1
fix: report a failure to attach credentials as an error
PreciousOritsedere Jul 30, 2026
b249937
chore: remove the reactive-auth spike harness
PreciousOritsedere Jul 30, 2026
a66259c
feat: sign in with only an issuer
PreciousOritsedere Jul 31, 2026
fc26579
fix: stop the login field and CSS script implying the wrong defaults
PreciousOritsedere Jul 31, 2026
911bafa
fix: clear the lint backlog
PreciousOritsedere Jul 31, 2026
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
156 changes: 11 additions & 145 deletions app/components/AuthWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,155 +1,21 @@
"use client";

import { useEffect, useState, Suspense } from "react";
import { useSolidAuth } from "@ldo/solid-react";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
import LoadingSpinner from "./shared/LoadingSpinner";
import { useEffect, type ReactNode } from "react";
import { useRouter } from "next/navigation";
import { useSolidAuth } from "../lib/hooks/useSolidAuth";
import FullPageLoader from "./shared/FullPageLoader";

interface AuthWrapperProps {
children: React.ReactNode;
}

// Check if there's any indication of a session in storage
function hasSessionInStorage(): boolean {
if (typeof window === "undefined") return false;

try {
const keys = Object.keys(localStorage);
// Look for keys that might indicate a session exists
return keys.some(key =>
key.includes("solidClientAuthn") ||
key.includes("solid-auth") ||
key.includes("oidc") ||
key.includes("session")
);
} catch {
return false;
}
}

function AuthWrapperContent({ children }: AuthWrapperProps) {
const { session } = useSolidAuth();
const searchParams = useSearchParams();
export default function AuthWrapper({ children }: { children: ReactNode }) {
const { status } = useSolidAuth();
const router = useRouter();
const pathname = usePathname();
const [isCheckingSession, setIsCheckingSession] = useState(true);
const [hasSessionIndicator, setHasSessionIndicator] = useState(() => hasSessionInStorage());
const [wasLoggedIn, setWasLoggedIn] = useState(false);

// Check if we're in the middle of an OAuth callback
const isOAuthCallback = searchParams.has("code") || searchParams.has("state");
const isLoginPage = pathname === "/login";


const hasSessionData = !!(session.webId || session.sessionId || session.clientAppId);

useEffect(() => {
if (session.isLoggedIn) {
setWasLoggedIn(true);
}

// If we're in an OAuth callback, keep checking until session is established
// Don't redirect until session is confirmed
if (isOAuthCallback) {
setIsCheckingSession(true);

if (session.isLoggedIn) {
setIsCheckingSession(false);
// Redirect to home after successful OAuth (remove OAuth params from URL)
const redirectTimer = setTimeout(() => {
if (typeof window !== "undefined") {
window.location.href = "/";
}
}, 200);
return () => clearTimeout(redirectTimer);
} else {
// Session not yet established, keep waiting
// Set a timeout to prevent infinite waiting (max 10 seconds)
const maxWaitTimer = setTimeout(() => {
setIsCheckingSession(false);
}, 10000);
return () => clearTimeout(maxWaitTimer);
}
}


if (session.isLoggedIn) {
setIsCheckingSession(false);
if (isLoginPage) {
router.replace("/");
}
return;
}


if (wasLoggedIn && !session.isLoggedIn) {
setIsCheckingSession(false);
if (!isLoginPage) {
router.replace("/login");
}
return;
}
if (status === "anonymous") router.replace("/login");
}, [status, router]);

// If we have session data (webId, sessionId, etc.) but isLoggedIn is false,
// the session is likely being restored - wait longer
// Otherwise, if no session data and no storage indicator, show login quickly
const shouldWaitForRestore = hasSessionData || hasSessionIndicator;
const checkTimer = setTimeout(() => {
setIsCheckingSession(false);
// Fail closed: children mount only once a session is confirmed, so nothing
// downstream can fetch before there is anything to authenticate with.
if (status !== "authenticated") return <FullPageLoader />;

if (!session.isLoggedIn && !isLoginPage && !isOAuthCallback) {
router.replace("/login");
}
}, shouldWaitForRestore ? 2000 : 200);

return () => clearTimeout(checkTimer);
}, [session.isLoggedIn, session.webId, session.sessionId, isOAuthCallback, hasSessionIndicator, hasSessionData, wasLoggedIn, isLoginPage, router]);


if (isCheckingSession || isOAuthCallback) {
return (
<div className="flex min-h-screen items-center justify-center bg-white">
<LoadingSpinner size="md" text="Loading..." />
</div>
);
}

// If OAuth callback is on login page, we're still processing - show loading
// This handles the case where OAuth redirects back to /login
if (isOAuthCallback && isLoginPage) {
return (
<div className="flex min-h-screen items-center justify-center bg-white">
<LoadingSpinner size="md" text="Loading..." />
</div>
);
}

// If not authenticated and not on login page, redirect will happen in useEffect
// If authenticated and on login page, redirect will happen in useEffect
// For now, just show children (or nothing if redirecting)
if (!session.isLoggedIn && !isLoginPage) {
return null; // Redirecting to login
}

if (session.isLoggedIn && isLoginPage) {
return null; // Redirecting to home
}

// User is authenticated and on correct page, show the app
return <>{children}</>;
}


export default function AuthWrapper({ children }: AuthWrapperProps) {
return (
<Suspense
fallback={
<div className="flex min-h-screen items-center justify-center bg-white">
<LoadingSpinner size="md" text="Loading..." />
</div>
}
>
<AuthWrapperContent>{children}</AuthWrapperContent>
</Suspense>
);
}
2 changes: 1 addition & 1 deletion app/components/DeleteConfirmDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function DeleteConfirmDialog({
<div className="flex-1">
<p className="text-sm text-gray-700 mb-2">
Are you sure you want to delete{" "}
<span className="font-medium">"{file.name}"</span>?
<span className="font-medium">&ldquo;{file.name}&rdquo;</span>?
</p>
{file.type === "folder" && (
<p className="text-sm text-gray-500">
Expand Down
34 changes: 16 additions & 18 deletions app/components/FileItemMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useRef, useEffect } from "react";
import { useState, useRef } from "react";
import Button from "./shared/Button";
import {
EllipsisVerticalIcon,
Expand All @@ -15,6 +15,20 @@ import {
import { useClickOutside } from "../lib/hooks";
import { FileItemData } from "./FileItem";

/** Roughly how tall the menu renders; used to decide which way it opens. */
const ESTIMATED_MENU_HEIGHT = 250;

/** Opens upward only when the menu would not fit below but would above. */
function placementFor(button: HTMLElement | null): "top" | "bottom" {
if (!button) return "bottom";

const { top, bottom } = button.getBoundingClientRect();
const spaceBelow = window.innerHeight - bottom;
return spaceBelow < ESTIMATED_MENU_HEIGHT && top > ESTIMATED_MENU_HEIGHT
? "top"
: "bottom";
}

interface FileItemMenuProps {
file: FileItemData;
onRename?: (file: FileItemData) => void;
Expand Down Expand Up @@ -49,23 +63,6 @@ export default function FileItemMenu({
refs: [menuRef, menuButtonRef],
});

useEffect(() => {
if (showMenu && menuButtonRef.current) {
const buttonRect = menuButtonRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const spaceBelow = viewportHeight - buttonRect.bottom;
const spaceAbove = buttonRect.top;
const estimatedMenuHeight = 250; // Approximate height of the menu

// If there's not enough space below but enough space above, show menu above
if (spaceBelow < estimatedMenuHeight && spaceAbove > estimatedMenuHeight) {
setMenuPosition("top");
} else {
setMenuPosition("bottom");
}
}
}, [showMenu]);

const handleAction = (action: ((file: FileItemData) => void) | undefined) => {
if (action) {
action(file);
Expand Down Expand Up @@ -143,6 +140,7 @@ export default function FileItemMenu({
aria-expanded={showMenu}
onClick={(e) => {
e.stopPropagation();
if (!showMenu) setMenuPosition(placementFor(menuButtonRef.current));
setShowMenu(!showMenu);
}}
className={position === "top-right" ? "bg-white/90 hover:bg-white shadow-sm" : ""}
Expand Down
2 changes: 0 additions & 2 deletions app/components/FileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import EmptyState from "./shared/EmptyState";

interface FileListProps {
files: FileItemData[];
currentPath: string;
onFileSelect: (file: FileItemData) => void;
onFileDoubleClick: (file: FileItemData) => void;
onFileRename?: (file: FileItemData) => void;
Expand All @@ -25,7 +24,6 @@ const VIEW_STORAGE_KEY = "solid-file-manager-view";

export default function FileList({
files,
currentPath,
onFileSelect,
onFileDoubleClick,
onFileRename,
Expand Down
Loading