From 4c4837cc776c62ca7605ef9a228ac8c9611fd540 Mon Sep 17 00:00:00 2001 From: shadrach Date: Wed, 4 Sep 2024 03:15:49 -0500 Subject: [PATCH 1/5] fix: type, config and UI issues --- react.d.ts | 7 +++++++ src/app/actions.tsx | 11 +---------- src/app/api/logout/route.ts | 27 +++++++++++++------------- src/app/login/page.tsx | 1 + src/components/molecules/Dashboard.tsx | 17 +++++++++++++--- src/components/molecules/LoginForm.tsx | 6 +++--- src/lib/config.ts | 2 +- 7 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 react.d.ts diff --git a/react.d.ts b/react.d.ts new file mode 100644 index 0000000..658135b --- /dev/null +++ b/react.d.ts @@ -0,0 +1,7 @@ +declare module "react-dom" { + export function useFormStatus(): { pending: boolean }; + export function useFormState( + action: (state: T, formData: FormData) => T, + initialState: T, + ): [T, (formData: FormData) => void]; +} \ No newline at end of file diff --git a/src/app/actions.tsx b/src/app/actions.tsx index fba39f3..425fe35 100644 --- a/src/app/actions.tsx +++ b/src/app/actions.tsx @@ -13,9 +13,6 @@ export type LoginUserData = { }; export async function login(prevState: any, formData: FormData) { - const cookie = cookies().get(AUTH_COOKIE_FIELDNAME); - console.log("cookie", cookie); - const email = formData.get("email") ?? prevState?.email; const code = formData.get("code"); @@ -29,13 +26,7 @@ export async function login(prevState: any, formData: FormData) { if (response.ok && response.user) { // Set cookie - cookies().set(AUTH_COOKIE_FIELDNAME, response.user.token, { - path: "/", - expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30 * 1), // 1 month - httpOnly: true, - secure: process.env.NEXT_ENV === "production", - domain: process.env.NODE_ENV === "production" ? ".desci.com" : undefined, - }); + cookies().set(AUTH_COOKIE_FIELDNAME, response.user.token); redirect(`/`); } diff --git a/src/app/api/logout/route.ts b/src/app/api/logout/route.ts index b04867d..664ee66 100644 --- a/src/app/api/logout/route.ts +++ b/src/app/api/logout/route.ts @@ -4,18 +4,17 @@ import { cookies } from "next/headers"; import { NextResponse } from "next/server"; export async function DELETE(_request: Request) { - try { - const logoutRes = await fetch(`${NODES_API_URL}/v1/auth/logout"`, { - method: "delete", - credentials: "include", - headers: { - cookie: cookies().toString(), - }, - }); - console.log("LOGOUT", logoutRes); - cookies().delete(AUTH_COOKIE_FIELDNAME); - return NextResponse.json({ ok: true }); - } catch (e) { - return NextResponse.json({ error: e }, { status: 500 }); - } + try { + const logoutRes = await fetch(`${NODES_API_URL}/v1/auth/logout`, { + method: "delete", + credentials: "include", + headers: { + cookie: cookies().toString(), + }, + }); + if (logoutRes.ok && logoutRes.status === 200) cookies().delete(AUTH_COOKIE_FIELDNAME); + return NextResponse.json({ ok: logoutRes.ok }); + } catch (e) { + return NextResponse.json({ error: e }, { status: 500 }); + } } diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index aee9250..cc0d26d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,6 +1,7 @@ "use client"; import LoginForm from "@/components/molecules/LoginForm"; +// @ts-ignore import { useFormState, useFormStatus } from "react-dom"; import { login, LoginUserData } from "@/app/actions"; diff --git a/src/components/molecules/Dashboard.tsx b/src/components/molecules/Dashboard.tsx index 96c01df..7050ce1 100644 --- a/src/components/molecules/Dashboard.tsx +++ b/src/components/molecules/Dashboard.tsx @@ -21,6 +21,7 @@ import { import { Switch } from "@/components/ui/switch"; import DoiRecords from "@/components/molecules/DoiRecords"; import { cn } from "@/lib/utils"; +import { useRouter } from "next/navigation"; interface Node { id: string; @@ -51,15 +52,21 @@ function Sidebar({ activeTab: string; setActiveTab: (tab: string) => void; }) { + const router = useRouter(); return (
- {["Nodes", "Users", "DOIs", "Settings",].map((tab) => ( + {["Nodes", "Users", "DOIs", "Settings"].map((tab) => ( @@ -68,6 +75,11 @@ function Sidebar({ @@ -75,7 +87,6 @@ function Sidebar({ ); } - function NodesTable() { return ( diff --git a/src/components/molecules/LoginForm.tsx b/src/components/molecules/LoginForm.tsx index 6b2b928..7952c2e 100644 --- a/src/components/molecules/LoginForm.tsx +++ b/src/components/molecules/LoginForm.tsx @@ -7,7 +7,7 @@ export default function LoginForm({ email, message, }: { - login: (formData: FormData) => void; + login: (formData: FormData) => Promise; pending: boolean; email: string; message?: string; @@ -34,7 +34,7 @@ export default function LoginForm({ id="email" name="email" type="email" - className="text-txt-focus border-2 border-border-neutral border-danger-text" + className="text-txt-focus border-2 border-border-neutral invalid:border-danger-text" placeholder="user@email.com" autoFocus required @@ -58,7 +58,7 @@ export default function LoginForm({ required placeholder="XXXXXX" disabled={pending} - className="text-txt-focus border-2 border-border-neutral border-danger-text" + className="text-txt-focus border-2 border-border-neutral invalid:border-danger-text" /> )} diff --git a/src/lib/config.ts b/src/lib/config.ts index 97ea756..073c251 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1 +1 @@ -export const NODES_API_URL = process.env.NODES_API_URL as string; \ No newline at end of file +export const NODES_API_URL = process.env.NEXT_PUBLIC_API_URL as string; \ No newline at end of file From a75d427bda7cb0365c11f30d958bc5b3c2c00740 Mon Sep 17 00:00:00 2001 From: shadrach-tayo Date: Wed, 4 Sep 2024 11:52:00 -0500 Subject: [PATCH 2/5] configure api utill --- package-lock.json | 25 ++++++++ package.json | 1 + src/apis/queries.tsx | 7 +++ src/app/Provider.tsx | 63 +++++++++++++++++++ src/app/layout.tsx | 14 ++--- src/components/molecules/DoiRecords.tsx | 84 +++++++++++++++---------- src/lib/config.ts | 2 +- 7 files changed, 151 insertions(+), 45 deletions(-) create mode 100644 src/apis/queries.tsx create mode 100644 src/app/Provider.tsx diff --git a/package-lock.json b/package-lock.json index da12012..aa2286f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", + "@tanstack/react-query": "^5.54.1", "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -1045,6 +1046,30 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.54.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.54.1.tgz", + "integrity": "sha512-hKS+WRpT5zBFip21pB6Jx1C0hranWQrbv5EJ7qPoiV5MYI3C8rTCqWC9DdBseiPT1JgQWh8Y55YthuYZNiw3Xw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.54.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.54.1.tgz", + "integrity": "sha512-SuMi4JBYv49UtmiRyqjxY7XAnE1qwLht9nlkC8sioxFXz5Uzj30lepiKf2mYXuXfC7fHYjTrAPkNx+427pRHXA==", + "dependencies": { + "@tanstack/query-core": "5.54.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", diff --git a/package.json b/package.json index 7b0f491..e6e8d42 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", + "@tanstack/react-query": "^5.54.1", "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/src/apis/queries.tsx b/src/apis/queries.tsx new file mode 100644 index 0000000..c04af49 --- /dev/null +++ b/src/apis/queries.tsx @@ -0,0 +1,7 @@ +import { NODES_API_URL } from "@/lib/config"; + +export async function getDois() { + const response = await fetch(`${NODES_API_URL}/v1/admin/doi/list`); + console.log("DOIs", response); + return response.json(); +} diff --git a/src/app/Provider.tsx b/src/app/Provider.tsx new file mode 100644 index 0000000..c5d5643 --- /dev/null +++ b/src/app/Provider.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { ThemeProvider } from "@/components/theme-provider"; +import { + isServer, + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query"; +import { PropsWithChildren, useState } from "react"; + +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 60 * 1000, + }, + }, + }); +} + +let browserQueryClient: QueryClient | undefined = undefined; + +function getQueryClient() { + if (isServer) { + // Server: always make a new query client + return makeQueryClient(); + } else { + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + return browserQueryClient; + } +} + +export default function Provider({ children }: PropsWithChildren<{}>) { + // const [queryClient] = useState( + // () => + // new QueryClient({ + // defaultOptions: { + // queries: { + // // With SSR, we usually want to set some default staleTime + // // above 0 to avoid refetching immediately on the client + // staleTime: 60 * 1000, + // }, + // }, + // }) + // ); + const queryClient = getQueryClient(); + return ( + + {children} + + ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a8b0065..c7252a7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,7 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; -import { ThemeProvider } from "@/components/theme-provider"; import "./globals.scss"; - +import Provider from "./Provider"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { @@ -10,6 +9,8 @@ export const metadata: Metadata = { description: "", }; +// const queryClient = new QueryClient(); + export default function RootLayout({ children, }: Readonly<{ @@ -18,14 +19,7 @@ export default function RootLayout({ return ( - - {children} - + {children} ); diff --git a/src/components/molecules/DoiRecords.tsx b/src/components/molecules/DoiRecords.tsx index 9df1e4b..963b535 100644 --- a/src/components/molecules/DoiRecords.tsx +++ b/src/components/molecules/DoiRecords.tsx @@ -1,43 +1,59 @@ -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +"use client"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { Button } from "@/components/ui/button"; +import { useQuery } from "@tanstack/react-query"; +import { getDois } from "@/apis/queries"; interface Doi { - id: string; - name: string; - status: "active" | "inactive"; - } + id: string; + name: string; + status: "active" | "inactive"; +} const mockDois: Doi[] = [ - { id: "1", name: "Doi 1", status: "active" }, - { id: "2", name: "Doi 2", status: "inactive" }, + { id: "1", name: "Doi 1", status: "active" }, + { id: "2", name: "Doi 2", status: "inactive" }, ]; export default function DoiRecords() { + const { data, isLoading, isError } = useQuery({ + queryKey: ["dois/list"], + queryFn: getDois, + }); + console.log("DOIs", { data, isLoading, isError }); return ( -
- - - Name - Status - Actions +
+ + + Name + Status + Actions + + + + {mockDois.map((node) => ( + + {node.name} + {node.status} + +
+ + + +
+
- - - {mockDois.map((node) => ( - - {node.name} - {node.status} - -
- - - -
-
-
- ))} -
-
- ); - } \ No newline at end of file + ))} + + + ); +} diff --git a/src/lib/config.ts b/src/lib/config.ts index 97ea756..b2dfcea 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1 +1 @@ -export const NODES_API_URL = process.env.NODES_API_URL as string; \ No newline at end of file +export const NODES_API_URL = process.env.NEXT_PUBLIC_API_URL as string; From 414ad16cc4506a89e2a031024283eac89a12c0c0 Mon Sep 17 00:00:00 2001 From: shadrach-tayo Date: Wed, 4 Sep 2024 12:35:39 -0500 Subject: [PATCH 3/5] admin doi view --- src/apis/queries.tsx | 20 ++++++- src/apis/types.ts | 7 +++ src/components/molecules/DoiRecords.tsx | 78 ++++++++++++++++++------- src/lib/config.ts | 2 + 4 files changed, 82 insertions(+), 25 deletions(-) create mode 100644 src/apis/types.ts diff --git a/src/apis/queries.tsx b/src/apis/queries.tsx index c04af49..dedf40e 100644 --- a/src/apis/queries.tsx +++ b/src/apis/queries.tsx @@ -1,7 +1,21 @@ import { NODES_API_URL } from "@/lib/config"; +import { DoiRecord } from "./types"; export async function getDois() { - const response = await fetch(`${NODES_API_URL}/v1/admin/doi/list`); - console.log("DOIs", response); - return response.json(); + const response = await fetch(`${NODES_API_URL}/v1/admin/doi/list`, { + credentials: "include", + }); + + const data = (await response.json()) as + | { + data: DoiRecord[]; + message: string; + } + | { message: string }; + + if (response.status === 200 && "data" in data) { + return data.data; + } + + throw new Error(data?.message); } diff --git a/src/apis/types.ts b/src/apis/types.ts new file mode 100644 index 0000000..ef2a270 --- /dev/null +++ b/src/apis/types.ts @@ -0,0 +1,7 @@ +export type DoiRecord = { + id: number; + doi: string; + dpid: string; + uuid: string; + createdAt: string | Date; +}; diff --git a/src/components/molecules/DoiRecords.tsx b/src/components/molecules/DoiRecords.tsx index 963b535..9cd3145 100644 --- a/src/components/molecules/DoiRecords.tsx +++ b/src/components/molecules/DoiRecords.tsx @@ -11,43 +11,77 @@ import { import { Button } from "@/components/ui/button"; import { useQuery } from "@tanstack/react-query"; import { getDois } from "@/apis/queries"; - -interface Doi { - id: string; - name: string; - status: "active" | "inactive"; -} -const mockDois: Doi[] = [ - { id: "1", name: "Doi 1", status: "active" }, - { id: "2", name: "Doi 2", status: "inactive" }, -]; +import Link from "next/link"; +import { ExternalLinkIcon } from "@radix-ui/react-icons"; +import { Loader2Icon } from "lucide-react"; +import { DPID_BASE_URL } from "@/lib/config"; export default function DoiRecords() { const { data, isLoading, isError } = useQuery({ queryKey: ["dois/list"], queryFn: getDois, }); - console.log("DOIs", { data, isLoading, isError }); + + if (isLoading) { + return ( +
+ +
+ ); + } + return ( - Name - Status + ID + DOI + DPID + UUID + Registered At Actions - {mockDois.map((node) => ( - - {node.name} - {node.status} + {data?.map((record) => ( + + {record.id} + +
+ {record.doi} +
+
+ +
+ DPID://{record.dpid} +
+
+ {record.uuid} + {new Date(record.createdAt).toDateString()} -
- - -
diff --git a/src/lib/config.ts b/src/lib/config.ts index b2dfcea..aea8f2c 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1 +1,3 @@ export const NODES_API_URL = process.env.NEXT_PUBLIC_API_URL as string; +export const DPID_BASE_URL = process.env + .NEXT_PUBLIC_DPID_URL_OVERRIDE as string; From 16da57059563487a53941c362d016d9853eff574 Mon Sep 17 00:00:00 2001 From: shadrach-tayo Date: Wed, 4 Sep 2024 12:50:11 -0500 Subject: [PATCH 4/5] enhance: form styling and better error messages and validation --- src/app/actions.tsx | 6 ++++++ src/app/login/page.tsx | 7 ++++++- src/components/molecules/LoginForm.tsx | 10 +++++----- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/app/actions.tsx b/src/app/actions.tsx index 425fe35..e497665 100644 --- a/src/app/actions.tsx +++ b/src/app/actions.tsx @@ -16,6 +16,12 @@ export async function login(prevState: any, formData: FormData) { const email = formData.get("email") ?? prevState?.email; const code = formData.get("code"); + if (!email?.endsWith("@desci.com")) + return { + ok: false, + error: "Unauthorised email domain (only desci.com emails are allowed)", + }; + const res = await fetch(`${API_URL}/v1/auth/magic`, { method: "POST", body: JSON.stringify({ email, code }), diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index cc0d26d..6d5ebf0 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -24,7 +24,12 @@ export default function Login() {

{!state?.email ? "Verify Email" : "Check your inbox"}

- + ); diff --git a/src/components/molecules/LoginForm.tsx b/src/components/molecules/LoginForm.tsx index 7952c2e..40e8180 100644 --- a/src/components/molecules/LoginForm.tsx +++ b/src/components/molecules/LoginForm.tsx @@ -7,7 +7,7 @@ export default function LoginForm({ email, message, }: { - login: (formData: FormData) => Promise; + login: (formData: FormData) => void; pending: boolean; email: string; message?: string; @@ -34,7 +34,7 @@ export default function LoginForm({ id="email" name="email" type="email" - className="text-txt-focus border-2 border-border-neutral invalid:border-danger-text" + className="text-txt-focus border-2 border-border-neutral focus:invalid:border-red-400 focus-visible:valid:border-btn-surface-primary-focus focus-visible:outline-none focus-visible:ring-0" placeholder="user@email.com" autoFocus required @@ -58,14 +58,14 @@ export default function LoginForm({ required placeholder="XXXXXX" disabled={pending} - className="text-txt-focus border-2 border-border-neutral invalid:border-danger-text" + className="text-txt-focus border-2 border-border-neutral focus:invalid:border-red-400 focus-visible:valid:border-btn-surface-primary-focus focus-visible:outline-none focus-visible:ring-0" /> )} - {message &&

{message}

} + {message &&

{message}

}