Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
205 changes: 37 additions & 168 deletions app/components/LoginPage.tsx

@jeswr jeswr Jul 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep this file named as just LoginPage the fact that it remembers the IDP is just a feature of the page.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have pushed a commit for this

Original file line number Diff line number Diff line change
@@ -1,175 +1,44 @@
"use client";

import { useState, useEffect } from "react";
import { useSolidAuth } from "@ldo/solid-react";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import Button from "./shared/Button";
import UrlCombobox, { ComboboxOption } from "./shared/UrlCombobox";
import GitHubLinks from "./shared/GitHubLinks";
import { SolidLoginPage } from "solid-react-component/login/next";
import { getLastIdp, saveLastIdp } from "../lib/helpers/idpHistoryUtils";

const PRESET_ISSUERS: ComboboxOption[] = [
{ label: "Solid Community", value: "https://solidcommunity.net/", secondaryLabel: "https://solidcommunity.net/" },
{ label: "Inrupt", value: "https://login.inrupt.com", secondaryLabel: "https://login.inrupt.com" },
];

export default function LoginPage() {
const { session, login } = useSolidAuth();
const router = useRouter();
const [issuerInput, setIssuerInput] = useState<string>(
process.env.NEXT_PUBLIC_OIDC_ISSUER || ""
);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

// Redirect to home if already authenticated
useEffect(() => {
if (session.isLoggedIn) {
router.replace("/");
}
}, [session.isLoggedIn, router]);

// Don't render login form if already authenticated (redirecting)
if (session.isLoggedIn) {
return null;
}

const validateIssuerUrl = (url: string): boolean => {
if (!url.trim()) {
setError("Please enter a Solid Identity Provider URL");
return false;
}

try {
const parsedUrl = new URL(url);
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
setError("URL must start with http:// or https://");
return false;
}
} catch {
setError("Please enter a valid URL");
return false;
}

setError(null);
return true;
};

const handleLogin = async () => {
const trimmedIssuer = issuerInput.trim();
if (!validateIssuerUrl(trimmedIssuer)) {
return;
}

setIsLoading(true);
try {
await login(trimmedIssuer);
} catch (error) {
console.error("Login failed:", error);
setIsLoading(false);
}
};

const handleIssuerChange = (value: string) => {
setIssuerInput(value);
if (error) {
setError(null);
}
};

return (
<main className="flex min-h-screen bg-white" role="main" aria-label="Sign in page">
{/* Left side - Logo and branding */}
<section
className="hidden flex-1 items-center justify-center border-r border-gray-200 bg-[#F3EDFF] px-8 lg:flex"
aria-label="Branding section"
>
<div className="max-w-md">
<header className="flex flex-col items-center justify-center gap-2">
<div className="flex items-center justify-center w-[200px] h-[200px]">
<Image
src="/file-manager-logo.svg"
alt="Solid File Manager Logo"
width={60}
height={60}
className="w-full h-full object-cover"
priority
/>
</div>
<h1 className="mb-2 text-4xl font-normal text-black">Sign in</h1>
<p className="text-base text-gray-600">
to continue to Solid File Manager
</p>
</header>
</div>
</section>

{/* Right side - Login form */}
<section
className="flex w-full flex-1 items-center justify-center bg-white px-4 py-12 lg:w-auto lg:min-w-[450px]"
aria-label="Sign in form section"
>
<div className="w-full max-w-md">
{/* Mobile logo */}
<header className="mb-8 lg:hidden flex flex-col items-center justify-center">
<div className="mb-2 flex items-center justify-center w-[200px] h-[200px]">
<Image
src="/file-manager-logo.svg"
alt="Solid File Manager Logo"
width={60}
height={60}
className="w-full h-full object-cover"
priority
/>
</div>
<h1 className="mb-2 text-center text-3xl font-normal text-black">Sign in</h1>
<p className="text-center text-base text-gray-600">
to continue to Solid File Manager
</p>
</header>

<form
onSubmit={(e) => {
e.preventDefault();
handleLogin();
}}
className="space-y-6"
aria-label="Sign in form"
noValidate
>
{/* Identity Provider Input */}
<UrlCombobox
id="oidc-issuer"
label="Solid Identity Provider"
value={issuerInput}
onChange={handleIssuerChange}
options={PRESET_ISSUERS}
placeholder="Enter your provider URL or select from the list"
error={error || undefined}
disabled={isLoading}
aria-label="Enter or select Solid Identity Provider"
const router = useRouter();
const [mounted, setMounted] = useState(false);
const [defaultIssuer, setDefaultIssuer] = useState("");

useEffect(() => {
setDefaultIssuer(getLastIdp());
setMounted(true);
}, []);

const handleSubmit = (event: React.FormEvent<HTMLDivElement>) => {
const input = event.currentTarget.querySelector("input");
if (input instanceof HTMLInputElement) {
saveLastIdp(input.value);
}
};

// Render only after localStorage is read, so defaultIssuer is applied on
// SolidLoginPage's initial mount (it reads defaultIssuer only once).
if (!mounted) return null;

return (
<div onSubmit={handleSubmit}>
<SolidLoginPage
onAlreadyLoggedIn={() => router.replace("/")}
defaultIssuer={defaultIssuer}
logo="/file-manager-logo.svg"
logoAlt="Solid File Manager Logo"
title="Sign in"
subtitle="to continue to Solid File Manager"
footerGitHubUrl="https://github.com/solid/solid-file-manager"
footerIssuesUrl="https://github.com/solid/solid-file-manager/issues/new"
/>

{/* Action button */}
<div className="flex items-center justify-end pt-4">
<Button
type="submit"
variant="primary"
disabled={isLoading}
isLoading={isLoading}
aria-label={isLoading ? "Signing in, please wait" : "Continue to sign in"}
>
{isLoading ? "Signing in..." : "Next"}
</Button>
</div>
</form>

{/* Footer links */}
<footer className="mt-24 flex flex-col items-center md:items-end">
<GitHubLinks layout="horizontal" />
</footer>
</div>
</section>
</main>
);
}
)

}
29 changes: 29 additions & 0 deletions app/lib/helpers/idpHistoryUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const LAST_IDP_KEY = "solid-file-manager-last-idp";

export function getLastIdp(): string {
if (typeof window === "undefined") return "";

try {
return localStorage.getItem(LAST_IDP_KEY)?.trim() ?? "";
} catch (error) {
console.warn(
"Could not read last identity provider from localStorage",
error,
);
return "";
}
}

export function saveLastIdp(issuer: string): void {
if (typeof window === "undefined") return;
const value = issuer.trim();
if (!value) return;
try {
localStorage.setItem(LAST_IDP_KEY, value);
} catch (error) {
console.warn(
"Could not save last identity provider to localStorage",
error,
);
}
}
12 changes: 2 additions & 10 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { useRouter } from "next/navigation";
import {
SolidLoginNavigationProviderNext,
AuthGuard,
SolidLoginPage,
} from "solid-react-component/login/next";
import LoginPage from "../components/LoginPage";

const loadingFallback = (
<div className="flex min-h-screen items-center justify-center bg-white">
Expand All @@ -22,15 +22,7 @@ export default function Login() {
config={{ loginPath: "/login", homePath: "/" }}
>
<AuthGuard fallback={loadingFallback}>
<SolidLoginPage
onAlreadyLoggedIn={() => router.replace("/")}
logo="/file-manager-logo.svg"
logoAlt="Solid File Manager Logo"
title="Sign in"
subtitle="to continue to Solid File Manager"
footerGitHubUrl="https://github.com/solid/solid-file-manager"
footerIssuesUrl="https://github.com/solid/solid-file-manager/issues/new"
/>
<LoginPage />
</AuthGuard>
</SolidLoginNavigationProviderNext>
</Suspense>
Expand Down