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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# admin
# admin

Administrative tools for storacha.network.

Expand All @@ -20,4 +20,3 @@ You can start editing the page by modifying `app/page.tsx`. The page auto-update

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

136 changes: 81 additions & 55 deletions app/customers/[did]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,115 @@
'use client'
"use client";

import Link from "next/link"
import { notFound } from "next/navigation"
import * as DidMailto from '@storacha/did-mailto'
import Link from "next/link";
import { notFound } from "next/navigation";
import * as DidMailto from "@storacha/did-mailto";

import { useCustomer } from "@/hooks/customer"
import { useRateLimitActions } from "@/hooks/rate-limit"
import { SimpleError } from "@/components/error"
import { Loader } from "@/components/brand"
import { useCustomer } from "@/hooks/customer";
import { useRateLimitActions } from "@/hooks/rate-limit";
import { SimpleError } from "@/components/error";
import { Loader } from "@/components/brand";
import { use } from "react";

export const runtime = 'edge'
export const runtime = "edge";

function domainFromEmail (email: string) {
const ind = email.indexOf('@')
return email.slice(ind + 1)
function domainFromEmail(email: string) {
const ind = email.indexOf("@");
return email.slice(ind + 1);
}

function mailtoDidFromUrlComponent (urlComponent: string) {
function mailtoDidFromUrlComponent(urlComponent: string) {
try {
return DidMailto.fromString(decodeURIComponent(urlComponent))
return DidMailto.fromString(decodeURIComponent(urlComponent));
} catch {
return undefined
return undefined;
}
}

export default function Customer ({ params: { did: encodedDid } }: { params: { did: string } }) {
const did = mailtoDidFromUrlComponent(encodedDid)
const email = did && DidMailto.toEmail(did)
const domain = email && domainFromEmail(email)

const { data: customer, error, isLoading } = useCustomer(did)
const { addBlock: addEmailBlock, removeBlock: removeEmailBlock, blocked: emailBlocked } = useRateLimitActions(email)
const { addBlock: addDomainBlock, removeBlock: removeDomainBlock, blocked: domainBlocked } = useRateLimitActions(domain)
export default function Customer(props: { params: Promise<{ did: string }> }) {
const { did: encodedDid } = use(props.params);
const did = mailtoDidFromUrlComponent(encodedDid);
const email = did && DidMailto.toEmail(did);
const domain = email && domainFromEmail(email);

console.log(did)
const { data: customer, error, isLoading } = useCustomer(did);
const {
addBlock: addEmailBlock,
removeBlock: removeEmailBlock,
blocked: emailBlocked,
} = useRateLimitActions(email);
const {
addBlock: addDomainBlock,
removeBlock: removeDomainBlock,
blocked: domainBlocked,
} = useRateLimitActions(domain);
console.log(isLoading, error, customer);
if (did) {
return (
<div className='flex flex-col items-center'>
<h2 className='text-2xl mb-4'>Customer {did}</h2>
<div className="flex flex-col items-center">
<h2 className="text-2xl mb-4">Customer {did}</h2>
{isLoading && <Loader />}
{error && <SimpleError>{error.message?.toString() || `Error loading ${did}`}</SimpleError>}
{error && (
<SimpleError>
<div className="max-w-xl">
{error.message?.toString() || `Error loading ${did}`}
</div>
</SimpleError>
)}
{customer && (
<div className='flex flex-col items-center'>
<div className='flex flex-row space-x-2 mt-4 mb-2'>
<button className='rounded bg-gray-500 px-2 py-1 hover:bg-gray-600 active:bg-gray-400'
onClick={() => emailBlocked ? removeEmailBlock() : addEmailBlock()}>
{emailBlocked ? 'Unblock' : 'Block'} {email}
<div className="flex flex-col items-center">
<div className="flex flex-row space-x-2 mt-4 mb-2">
<button
className="rounded bg-gray-500 px-2 py-1 hover:bg-gray-600 active:bg-gray-400"
onClick={() =>
emailBlocked ? removeEmailBlock() : addEmailBlock()
}
>
{emailBlocked ? "Unblock" : "Block"} {email}
</button>
<button className='rounded bg-gray-500 px-2 py-1 hover:bg-gray-600 active:bg-gray-400'
onClick={() => domainBlocked ? removeDomainBlock() : addDomainBlock()}>
{domainBlocked ? 'Unblock' : 'Block'} {domain}
<button
className="rounded bg-gray-500 px-2 py-1 hover:bg-gray-600 active:bg-gray-400"
onClick={() =>
domainBlocked ? removeDomainBlock() : addDomainBlock()
}
>
{domainBlocked ? "Unblock" : "Block"} {domain}
</button>
</div>
<table className='border-separate border-spacing-x-4 mb-8'>
<table className="border-separate border-spacing-x-4 mb-8">
<tbody>
<tr>
<td className='font-bold'>Email blocked</td>
<td className='text-right'>{emailBlocked ? 'yes' : 'no'}</td>
<td className="font-bold">Email blocked</td>
<td className="text-right">{emailBlocked ? "yes" : "no"}</td>
</tr>
<tr>
<td className='font-bold'>Domain blocked</td>
<td className='text-right'>{domainBlocked ? 'yes' : 'no'}</td>
<td className="font-bold">Domain blocked</td>
<td className="text-right">{domainBlocked ? "yes" : "no"}</td>
</tr>
</tbody>
</table>
<h3 className='text-xl mb-2'>Subscriptions</h3>
<table className='border-separate border-spacing-x-4'>
<h3 className="text-xl mb-2">Subscriptions</h3>
<table className="border-separate border-spacing-x-4">
<tbody>
{
customer.subscriptions?.map(subscriptionId => (
<tr key={subscriptionId}>
<td>
<Link className='underline text-blue-200' href={`/subscriptions/${subscriptionId}`}>
{subscriptionId}
</Link>
</td>
</tr>
))
}
{customer.subscriptions?.map((subscriptionId) => (
<tr key={subscriptionId}>
<td>
<Link
className="underline text-blue-200"
href={`/subscriptions/${subscriptionId}`}
>
{subscriptionId}
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)
);
} else {
return notFound()
return notFound();
}
}
}
4 changes: 4 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
.btn {
@apply rounded bg-gray-500 px-2 py-1 hover:bg-gray-600 active:bg-gray-400 cursor-pointer;
}

input {
@apply bg-white;
}
}
53 changes: 33 additions & 20 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
'use client'
"use client";

import './globals.css'
import type { JSX } from 'react'
import { AgentProvider } from '@/contexts/agent'
import { ServiceProvider } from '@/contexts/service'
import Nav from '@/components/nav'
import "./globals.css";
import type { JSX } from "react";
import { AgentProvider } from "@/contexts/agent";
import { ServiceProvider } from "@/contexts/service";
import Nav from "@/components/nav";
import {
Authenticator,
Provider as StorachaProvider,
} from "@storacha/ui-react";
import { AuthenticationEnsurer } from "@/components/Authenticator";

export default function RootLayout ({ children }: { children: React.ReactNode }): JSX.Element {
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
return (
<ServiceProvider>
<AgentProvider>
<html>
<body className='min-h-screen bg-slate-800 text-white'>
<Nav />
<main className='grow text-white p-4'>
{children}
</main>
</body>
</html>
</AgentProvider>
</ServiceProvider>
)
<StorachaProvider>
<Authenticator>
<ServiceProvider>
<AgentProvider>
<html>
<body className="min-h-screen bg-slate-800 text-white">
<Nav />
<main className="grow text-white p-4">
<AuthenticationEnsurer>{children}</AuthenticationEnsurer>
</main>
</body>
</html>
</AgentProvider>
</ServiceProvider>
</Authenticator>
</StorachaProvider>
);
}
1 change: 0 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { EmailAddress } from "@storacha/did-mailto";
import { useState, useCallback, ChangeEvent, FormEvent } from "react"
import { useRouter } from "next/navigation";
import * as MailtoDid from '@storacha/did-mailto'
import Link from "next/link";

export default function Root () {
const router = useRouter();
Expand Down
4 changes: 3 additions & 1 deletion app/spaces/[did]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import { Loader } from "@/components/brand"
import { SimpleError } from "@/components/error"
import { useConsumer } from "@/hooks/consumer"
import { useRateLimitActions, useRateLimits } from "@/hooks/rate-limit"

Check warning on line 6 in app/spaces/[did]/page.tsx

View workflow job for this annotation

GitHub Actions / test / Test

'useRateLimits' is defined but never used
import { DIDKey } from "@ucanto/interface"
import Link from "next/link"
import { use } from "react"

export const runtime = 'edge'

export default function Space ({ params: { did: encodedDid } }: { params: { did: string } }) {
export default function Space (props: { params: Promise<{ did: string }> }) {
const {did: encodedDid} = use(props.params)
const did = decodeURIComponent(encodedDid)
const { data: space, error, isLoading } = useConsumer(did as DIDKey)
const { addBlock, removeBlock, blocked } = useRateLimitActions(did)
Expand Down
73 changes: 73 additions & 0 deletions components/Authenticator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ReactNode } from 'react'
import { Authenticator, useAuthenticator } from '@storacha/ui-react'
import { Loader } from './Loader'

export function AuthenticationForm(): ReactNode {
const [{ submitted }] = useAuthenticator()
return (
<div className="authenticator">
<Authenticator.Form className="text-zinc-950 bg-grad rounded-xl shadow-md px-10 pt-8 pb-8">
<div>
<label
className="block mb-2 uppercase text-xs font-semibold tracking-wider m-1 font-mono"
htmlFor="authenticator-email"
>
Email
</label>
<Authenticator.EmailInput
className="text-black py-2 px-2 rounded block mb-4 border border-gray-800 w-80 shadow-md"
id="authenticator-email"
required
/>
</div>
<div className="text-center mt-4">
<button
className="inline-block bg-zinc-950 hover:outline text-white font-bold text-sm px-6 py-2 rounded-full whitespace-nowrap"
type="submit"
disabled={submitted}
>
Authorize
</button>
</div>
</Authenticator.Form>
</div>
)
}

export function AuthenticationSubmitted(): ReactNode {
const [{ email }] = useAuthenticator()
return (
<div className="authenticator">
<div className="bg-grad rounded-xl shadow-md px-10 pt-8 pb-8">
<h1 className="text-xl font-semibold">Verify your email address!</h1>
<p className="pt-2 pb-4">
Click the link in the email we sent to{' '}
<span className="font-semibold tracking-wide">{email}</span> to
authorize this agent.
</p>
<Authenticator.CancelButton className="inline-block bg-zinc-950 hover:outline text-white font-bold text-sm px-6 py-2 rounded-full whitespace-nowrap">
Cancel
</Authenticator.CancelButton>
</div>
</div>
)
}

export function AuthenticationEnsurer({
children,
}: {
children: ReactNode
}): ReactNode {
const [{ submitted, accounts, client }] = useAuthenticator()
const authenticated = accounts.length > 0
if (authenticated) {
return <>{children}</>
}
if (submitted) {
return <AuthenticationSubmitted />
}
if (client != null) {
return <AuthenticationForm />
}
return <Loader />
}
46 changes: 46 additions & 0 deletions components/Loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ReactNode } from 'react'
import { ArrowPathIcon } from '@heroicons/react/20/solid'
import { ProgressStatus, UploadProgress } from '@storacha/ui-react'

function StatusLoader({
progressStatus,
}: {
progressStatus: ProgressStatus
}): ReactNode {
const { total, loaded, lengthComputable } = progressStatus
if (lengthComputable) {
const percentComplete = Math.floor((loaded / total) * 100)
return (
<div className="relative w2 h5 ba b--white flex flex-column justify-end">
<div
className="bg-white w100"
style={{ height: `${percentComplete}%` }}
></div>
</div>
)
} else {
return <ArrowPathIcon className="animate-spin h-4 w-4" />
}
}

interface LoaderProps {
uploadProgress: UploadProgress
className?: string
}

export function UploadLoader({
uploadProgress,
className = '',
}: LoaderProps): ReactNode {
return (
<div className={`${className} flex flex-row`}>
{Object.values(uploadProgress).map((status) => (
<StatusLoader progressStatus={status} key={status.url} />
))}
</div>
)
}

export function Loader(): ReactNode {
return <ArrowPathIcon className="animate-spin h-12 w-12 mx-auto mt-12" />
}
Loading
Loading