Skip to content

Commit 2c73248

Browse files
Merge pull request #36 from solid/feat/my-storages
Updated login auth to use ldo-solid-react
2 parents f16f802 + ea6e6d4 commit 2c73248

18 files changed

Lines changed: 488 additions & 232 deletions

File tree

app/components/AuthWrapper.tsx

Lines changed: 120 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,155 @@
11
"use client";
22

3-
import { useEffect, useState } from "react";
4-
import { handleIncomingRedirect } from "@inrupt/solid-client-authn-browser";
5-
import { getSession } from "../lib/helpers";
6-
import LoginPage from "./LoginPage";
3+
import { useEffect, useState, Suspense } from "react";
4+
import { useSolidAuth } from "@ldo/solid-react";
5+
import { useSearchParams, useRouter, usePathname } from "next/navigation";
76
import LoadingSpinner from "./shared/LoadingSpinner";
8-
import ErrorDisplay from "./shared/ErrorDisplay";
97

108
interface AuthWrapperProps {
119
children: React.ReactNode;
1210
}
1311

14-
export default function AuthWrapper({ children }: AuthWrapperProps) {
15-
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
16-
const [isChecking, setIsChecking] = useState(true);
17-
const [error, setError] = useState<Error | null>(null);
12+
// Check if there's any indication of a session in storage
13+
function hasSessionInStorage(): boolean {
14+
if (typeof window === "undefined") return false;
15+
16+
try {
17+
const keys = Object.keys(localStorage);
18+
// Look for keys that might indicate a session exists
19+
return keys.some(key =>
20+
key.includes("solidClientAuthn") ||
21+
key.includes("solid-auth") ||
22+
key.includes("oidc") ||
23+
key.includes("session")
24+
);
25+
} catch {
26+
return false;
27+
}
28+
}
1829

19-
useEffect(() => {
20-
async function checkAuth() {
21-
try {
22-
setError(null);
30+
function AuthWrapperContent({ children }: AuthWrapperProps) {
31+
const { session } = useSolidAuth();
32+
const searchParams = useSearchParams();
33+
const router = useRouter();
34+
const pathname = usePathname();
35+
const [isCheckingSession, setIsCheckingSession] = useState(true);
36+
const [hasSessionIndicator, setHasSessionIndicator] = useState(() => hasSessionInStorage());
37+
const [wasLoggedIn, setWasLoggedIn] = useState(false);
2338

24-
// First, handle any incoming OAuth redirect (processes code and state parameters)
25-
await handleIncomingRedirect({ restorePreviousSession: true });
39+
// Check if we're in the middle of an OAuth callback
40+
const isOAuthCallback = searchParams.has("code") || searchParams.has("state");
41+
const isLoginPage = pathname === "/login";
2642

27-
// Get the session instance after handling redirect
28-
const session = getSession();
43+
44+
const hasSessionData = !!(session.webId || session.sessionId || session.clientAppId);
2945

30-
let isLoggedIn = session.info.isLoggedIn && !!session.info.webId;
46+
useEffect(() => {
47+
if (session.isLoggedIn) {
48+
setWasLoggedIn(true);
49+
}
3150

32-
// Check expiration if session exists
33-
if (isLoggedIn && session.info.expirationDate) {
34-
const expirationDate = new Date(session.info.expirationDate);
35-
const now = new Date();
36-
if (expirationDate <= now) {
37-
isLoggedIn = false;
51+
// If we're in an OAuth callback, keep checking until session is established
52+
// Don't redirect until session is confirmed
53+
if (isOAuthCallback) {
54+
setIsCheckingSession(true);
55+
56+
if (session.isLoggedIn) {
57+
setIsCheckingSession(false);
58+
// Redirect to home after successful OAuth (remove OAuth params from URL)
59+
const redirectTimer = setTimeout(() => {
60+
if (typeof window !== "undefined") {
61+
window.location.href = "/";
3862
}
39-
}
40-
41-
setIsAuthenticated(isLoggedIn);
42-
} catch (err) {
43-
const errorMessage =
44-
err instanceof Error ? err : new Error("Authentication check failed");
45-
setError(errorMessage);
46-
setIsAuthenticated(false);
47-
} finally {
48-
setIsChecking(false);
63+
}, 200);
64+
return () => clearTimeout(redirectTimer);
65+
} else {
66+
// Session not yet established, keep waiting
67+
// Set a timeout to prevent infinite waiting (max 10 seconds)
68+
const maxWaitTimer = setTimeout(() => {
69+
setIsCheckingSession(false);
70+
}, 10000);
71+
return () => clearTimeout(maxWaitTimer);
4972
}
5073
}
5174

52-
checkAuth();
53-
}, []);
54-
55-
// Re-check authentication state periodically in case user logs in from another tab
56-
useEffect(() => {
57-
if (!isAuthenticated && !error) {
58-
const interval = setInterval(async () => {
59-
try {
60-
const session = getSession();
61-
if (session.info.isLoggedIn) {
62-
setIsAuthenticated(true);
63-
setError(null);
64-
}
65-
} catch (err) {
66-
// Silent fail for polling
67-
}
68-
}, 1000);
75+
76+
if (session.isLoggedIn) {
77+
setIsCheckingSession(false);
78+
if (isLoginPage) {
79+
router.replace("/");
80+
}
81+
return;
82+
}
6983

70-
return () => clearInterval(interval);
84+
85+
if (wasLoggedIn && !session.isLoggedIn) {
86+
setIsCheckingSession(false);
87+
if (!isLoginPage) {
88+
router.replace("/login");
89+
}
90+
return;
7191
}
72-
}, [isAuthenticated, error]);
73-
74-
const handleRetry = () => {
75-
setError(null);
76-
setIsChecking(true);
77-
setIsAuthenticated(null);
78-
79-
handleIncomingRedirect({ restorePreviousSession: true })
80-
.then(() => {
81-
const session = getSession();
82-
setIsAuthenticated(session.info.isLoggedIn);
83-
})
84-
.catch((err) => {
85-
const errorMessage =
86-
err instanceof Error ? err : new Error("Authentication check failed");
87-
setError(errorMessage);
88-
setIsAuthenticated(false);
89-
})
90-
.finally(() => {
91-
setIsChecking(false);
92-
});
93-
};
94-
95-
if (isChecking) {
92+
93+
// If we have session data (webId, sessionId, etc.) but isLoggedIn is false,
94+
// the session is likely being restored - wait longer
95+
// Otherwise, if no session data and no storage indicator, show login quickly
96+
const shouldWaitForRestore = hasSessionData || hasSessionIndicator;
97+
const checkTimer = setTimeout(() => {
98+
setIsCheckingSession(false);
99+
100+
if (!session.isLoggedIn && !isLoginPage && !isOAuthCallback) {
101+
router.replace("/login");
102+
}
103+
}, shouldWaitForRestore ? 2000 : 200);
104+
105+
return () => clearTimeout(checkTimer);
106+
}, [session.isLoggedIn, session.webId, session.sessionId, isOAuthCallback, hasSessionIndicator, hasSessionData, wasLoggedIn, isLoginPage, router]);
107+
108+
109+
if (isCheckingSession || isOAuthCallback) {
96110
return (
97111
<div className="flex min-h-screen items-center justify-center bg-white">
98112
<LoadingSpinner size="md" text="Loading..." />
99113
</div>
100114
);
101115
}
102116

103-
if (error) {
117+
// If OAuth callback is on login page, we're still processing - show loading
118+
// This handles the case where OAuth redirects back to /login
119+
if (isOAuthCallback && isLoginPage) {
104120
return (
105-
<ErrorDisplay
106-
title="Authentication Error"
107-
message={error.message || "Failed to authenticate. Please try again."}
108-
onRetry={handleRetry}
109-
/>
121+
<div className="flex min-h-screen items-center justify-center bg-white">
122+
<LoadingSpinner size="md" text="Loading..." />
123+
</div>
110124
);
111125
}
112126

113-
if (!isAuthenticated) {
114-
return <LoginPage />;
127+
// If not authenticated and not on login page, redirect will happen in useEffect
128+
// If authenticated and on login page, redirect will happen in useEffect
129+
// For now, just show children (or nothing if redirecting)
130+
if (!session.isLoggedIn && !isLoginPage) {
131+
return null; // Redirecting to login
115132
}
116133

134+
if (session.isLoggedIn && isLoginPage) {
135+
return null; // Redirecting to home
136+
}
137+
138+
// User is authenticated and on correct page, show the app
117139
return <>{children}</>;
118140
}
119141

142+
143+
export default function AuthWrapper({ children }: AuthWrapperProps) {
144+
return (
145+
<Suspense
146+
fallback={
147+
<div className="flex min-h-screen items-center justify-center bg-white">
148+
<LoadingSpinner size="md" text="Loading..." />
149+
</div>
150+
}
151+
>
152+
<AuthWrapperContent>{children}</AuthWrapperContent>
153+
</Suspense>
154+
);
155+
}

app/components/LoginPage.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"use client";
22

3-
import { useState } from "react";
4-
import { login } from "@inrupt/solid-client-authn-browser";
3+
import { useState, useEffect } from "react";
4+
import { useSolidAuth } from "@ldo/solid-react";
5+
import { useRouter } from "next/navigation";
56
import Image from "next/image";
67
import Button from "./shared/Button";
78
import UrlCombobox, { ComboboxOption } from "./shared/UrlCombobox";
@@ -12,12 +13,26 @@ const PRESET_ISSUERS: ComboboxOption[] = [
1213
];
1314

1415
export default function LoginPage() {
16+
const { session, login } = useSolidAuth();
17+
const router = useRouter();
1518
const [issuerInput, setIssuerInput] = useState<string>(
1619
process.env.NEXT_PUBLIC_OIDC_ISSUER || ""
1720
);
1821
const [isLoading, setIsLoading] = useState(false);
1922
const [error, setError] = useState<string | null>(null);
2023

24+
// Redirect to home if already authenticated
25+
useEffect(() => {
26+
if (session.isLoggedIn) {
27+
router.replace("/");
28+
}
29+
}, [session.isLoggedIn, router]);
30+
31+
// Don't render login form if already authenticated (redirecting)
32+
if (session.isLoggedIn) {
33+
return null;
34+
}
35+
2136
const validateIssuerUrl = (url: string): boolean => {
2237
if (!url.trim()) {
2338
setError("Please enter a Solid Identity Provider URL");
@@ -47,12 +62,7 @@ export default function LoginPage() {
4762

4863
setIsLoading(true);
4964
try {
50-
const baseUrl = window.location.origin + window.location.pathname;
51-
await login({
52-
oidcIssuer: trimmedIssuer,
53-
clientName: "Solid File Manager",
54-
redirectUrl: baseUrl,
55-
});
65+
await login(trimmedIssuer);
5666
} catch (error) {
5767
console.error("Login failed:", error);
5868
setIsLoading(false);
@@ -157,4 +167,3 @@ export default function LoginPage() {
157167
</main>
158168
);
159169
}
160-

app/components/ProfileIcon.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11
"use client";
22

3-
import { useState, useEffect, useRef } from "react";
4-
import { logout } from "@inrupt/solid-client-authn-browser";
5-
import { getSession } from "../lib/helpers";
3+
import { useState, useRef } from "react";
4+
import { useSolidAuth } from "@ldo/solid-react";
65
import { useUserProfile, useClickOutside } from "../lib/hooks";
76
import { UserCircleIcon, ArrowRightStartOnRectangleIcon, PhoneIcon, BuildingOfficeIcon, BriefcaseIcon, GlobeAltIcon, ClipboardIcon } from "@heroicons/react/24/outline";
87
import toast from "react-hot-toast";
98

109
export default function ProfileIcon() {
10+
const { session, logout } = useSolidAuth();
1111
const [showTooltip, setShowTooltip] = useState(false);
1212
const [showDropdown, setShowDropdown] = useState(false);
1313
const { profile } = useUserProfile();
14-
const [webId, setWebId] = useState<string | null>(null);
1514
const dropdownRef = useRef<HTMLDivElement>(null);
1615
const tooltipRef = useRef<HTMLDivElement>(null);
1716
const buttonRef = useRef<HTMLButtonElement>(null);
1817

19-
useEffect(() => {
20-
const session = getSession();
21-
if (session.info.webId) {
22-
setWebId(session.info.webId);
23-
}
24-
}, []);
18+
const webId = session.webId || null;
2519

2620
useClickOutside({
2721
isEnabled: showDropdown,
@@ -32,9 +26,11 @@ export default function ProfileIcon() {
3226
const handleLogout = async () => {
3327
try {
3428
await logout();
35-
window.location.href = "/";
29+
// Redirect to login page after logout
30+
window.location.href = "/login";
3631
} catch (error) {
3732
console.error("Logout failed:", error);
33+
toast.error("Failed to sign out");
3834
}
3935
};
4036

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use client";
2+
3+
import { BrowserSolidLdoProvider } from "@ldo/solid-react";
4+
5+
export default function LdoProvider({
6+
children,
7+
}: {
8+
children: React.ReactNode;
9+
}) {
10+
return <BrowserSolidLdoProvider>{children}</BrowserSolidLdoProvider>;
11+
}
12+

app/layout.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
22
import { Geist, Geist_Mono } from "next/font/google";
33
import "./globals.css";
44
import Toaster from "./components/shared/Toaster";
5+
import LdoProvider from "./components/providers/LdoProvider";
56

67
const geistSans = Geist({
78
variable: "--font-geist-sans",
@@ -17,9 +18,9 @@ export const metadata: Metadata = {
1718
title: "Solid File Manager",
1819
description: "A Google Drive-like file manager for Solid Pods",
1920
icons: {
20-
icon: "/favicon.svg",
21-
shortcut: "/favicon.svg",
22-
apple: "/favicon.svg",
21+
icon: "/file-manager-logo.svg",
22+
shortcut: "/file-manager-logo.svg",
23+
apple: "/file-manager-logo.svg",
2324
},
2425
};
2526

@@ -33,8 +34,10 @@ export default function RootLayout({
3334
<body
3435
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
3536
>
36-
{children}
37-
<Toaster />
37+
<LdoProvider>
38+
{children}
39+
<Toaster />
40+
</LdoProvider>
3841
</body>
3942
</html>
4043
);

0 commit comments

Comments
 (0)