Skip to content
Open
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
62 changes: 60 additions & 2 deletions src/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean | null>(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 (
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: 'calc(100vh - 4rem)',
color: '#000'
}}>
Loading...
</div>
);
}

if (!isAuthenticated) {
return <AuthenticateUser />;
}

return <ListEditor />;
}
64 changes: 64 additions & 0 deletions src/components/ui/AuthenticateUser.tsx
Original file line number Diff line number Diff line change
@@ -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<string>(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 (
<main className={style.container}>
<section className={style.card}>

<div className={style.form}>
<label className={style.label}>
<span className={style.label_text}>Solid Identity Provider</span>
<select
value={selectedIssuer}
onChange={(e) => setSelectedIssuer(e.target.value)}
className={style.select}
disabled={isLoading}
>
{OIDC_ISSUERS.map((issuer) => (
<option key={issuer.value} value={issuer.value}>
{issuer.label}
</option>
))}
</select>
</label>

<button
type="button"
onClick={handleLogin}
className={style.login_button}
disabled={isLoading}
>
{isLoading ? "Logging in..." : "Login"}
</button>
</div>
</section>
</main>
);
}

37 changes: 18 additions & 19 deletions src/components/ui/ListEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
134 changes: 134 additions & 0 deletions src/styles/AuthenticateUserStyle.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
}