diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index ece2db4..2e6c812 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,11 +1,69 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { getDefaultSession, handleIncomingRedirect } from "@inrupt/solid-client-authn-browser"; import { ListEditor } from "../../components/ui/ListEditor"; +import { AuthenticateUser } from "../../components/ui/AuthenticateUser"; /** * This is the admin page. * It is available (by default) at http://localhost:3000/admin * It is used to create and delete list items. - * Actual functionality is in the ListEditor component. + * Shows authentication UI if user is not authenticated, otherwise shows ListEditor. */ -export default function () { +export default function AdminPage() { + const [isAuthenticated, setIsAuthenticated] = useState(null); + const [isChecking, setIsChecking] = useState(true); + + useEffect(() => { + async function checkAuth() { + try { + await handleIncomingRedirect(); + const session = getDefaultSession(); + setIsAuthenticated(session.info.isLoggedIn); + } catch (error) { + console.error("Auth check failed:", error); + setIsAuthenticated(false); + } finally { + setIsChecking(false); + } + } + + checkAuth(); + }, []); + + // Re-check authentication state periodically in case user logs in from another tab + useEffect(() => { + if (!isAuthenticated) { + const interval = setInterval(async () => { + await handleIncomingRedirect(); + const session = getDefaultSession(); + if (session.info.isLoggedIn) { + setIsAuthenticated(true); + } + }, 1000); + + return () => clearInterval(interval); + } + }, [isAuthenticated]); + + if (isChecking) { + return ( +
+ Loading... +
+ ); + } + + if (!isAuthenticated) { + return ; + } + return ; } diff --git a/src/components/ui/AuthenticateUser.tsx b/src/components/ui/AuthenticateUser.tsx new file mode 100644 index 0000000..6b654e8 --- /dev/null +++ b/src/components/ui/AuthenticateUser.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { useState } from "react"; +import { login } from "@inrupt/solid-client-authn-browser"; +import style from "../../styles/AuthenticateUserStyle.module.css"; + +const OIDC_ISSUERS = [ + { label: "Solid Community", value: "https://solidcommunity.net/" }, + { label: "Inrupt", value: "https://login.inrupt.com" }, +] as const; + +export function AuthenticateUser() { + const [selectedIssuer, setSelectedIssuer] = useState(OIDC_ISSUERS[0].value); + const [isLoading, setIsLoading] = useState(false); + + const handleLogin = async () => { + setIsLoading(true); + try { + await login({ + oidcIssuer: selectedIssuer, + clientName: "Solid List Items App", + redirectUrl: window.location.href, + }); + } catch (error) { + console.error("Login failed:", error); + setIsLoading(false); + } + }; + + return ( +
+
+ +
+ + + +
+
+
+ ); +} + diff --git a/src/components/ui/ListEditor.tsx b/src/components/ui/ListEditor.tsx index 4586bf5..3084f49 100644 --- a/src/components/ui/ListEditor.tsx +++ b/src/components/ui/ListEditor.tsx @@ -2,11 +2,7 @@ import type { FormEvent } from "react"; import { toTurtle } from "@ldo/ldo"; -import { - login, - getDefaultSession, - handleIncomingRedirect, -} from "@inrupt/solid-client-authn-browser"; +import { getDefaultSession, handleIncomingRedirect } from "@inrupt/solid-client-authn-browser"; import { useEffect, useState } from "react"; import { List, Item } from "../../ldo/Model.typings"; import { Config } from "../../Config"; @@ -25,7 +21,23 @@ export function ListEditor() { const [isFormOpen, setIsFormOpen] = useState(false); useEffect(() => { - authenticate().then(fetchList).then(setList); + async function initialize() { + // Handle redirect after authentication + await handleIncomingRedirect(); + const session = getDefaultSession(); + + // Only proceed if authenticated + if (session.info.isLoggedIn) { + try { + const fetchedList = await fetchList(); + setList(fetchedList); + } catch (error) { + console.error("Failed to fetch list:", error); + } + } + } + + initialize(); }, []); // Check if list is empty @@ -170,19 +182,6 @@ export function ListEditor() { ); } - async function authenticate() { - await handleIncomingRedirect(); - const session = getDefaultSession(); - - if (!session.info.isLoggedIn) { - console.log("unauthenticated"); - - await login({ - oidcIssuer: Config.oidcIssuer, - clientName: "My application", - }); - } - } async function save() { if (!list) { diff --git a/src/styles/AuthenticateUserStyle.module.css b/src/styles/AuthenticateUserStyle.module.css new file mode 100644 index 0000000..0bf401c --- /dev/null +++ b/src/styles/AuthenticateUserStyle.module.css @@ -0,0 +1,134 @@ +/* Authenticate User Component Styles */ + +.container { + display: flex; + align-items: center; + justify-content: center; + min-height: calc(100vh - 4rem); + padding: 2rem; +} + +.card { + background: white; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 16px; + padding: 3rem; + max-width: 500px; + width: 100%; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08); +} + +.title { + font-size: 2rem; + font-weight: 700; + color: #000; + margin: 0 0 1rem 0; + letter-spacing: -0.02em; +} + +.description { + color: #000; + margin: 0 0 2rem 0; + line-height: 1.6; +} + +.form { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.label { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.label_text { + font-weight: 600; + color: #000; + font-size: 0.95rem; +} + +.select { + padding: 0.875rem 1rem; + border: 2px solid rgba(0, 0, 0, 0.2); + border-radius: 8px; + font-size: 1rem; + font-family: inherit; + background: white; + color: #000; + cursor: pointer; + transition: all 0.2s ease; +} + +.select:focus { + outline: none; + border-color: #6f6d6d; + box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1); +} + +.select:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.login_button { + padding: 1rem 2rem; + background: black; + color: #fff; + border: 2px solid #000; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.login_button:hover:not(:disabled) { + background: #000; + color: white; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.login_button:focus-visible { + outline: 2px solid #000; + outline-offset: 2px; +} + +.login_button:active:not(:disabled) { + background: #333; + color: white; +} + +.login_button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .card { + padding: 2rem; + } + + .title { + font-size: 1.75rem; + } +} + +@media (max-width: 480px) { + .card { + padding: 1.5rem; + } + + .title { + font-size: 1.5rem; + } +} +