From b541573d77b34cdedaaec7a4a2415febeba4a655 Mon Sep 17 00:00:00 2001 From: SevicheCC <91365763+Sevichecc@users.noreply.github.com> Date: Mon, 5 Jun 2023 16:08:57 +0800 Subject: [PATCH] feat: add write adn read scopes to admin --- app/page.tsx | 3 + components/InputForm.tsx | 120 ++++++++++++++++++++++++++--------- components/ScopeCheckbox.tsx | 28 ++++++++ components/ScopeSection.tsx | 66 +++++++++---------- lib/types.ts | 28 -------- lib/utils.ts | 76 ++++++++++++---------- 6 files changed, 195 insertions(+), 126 deletions(-) create mode 100644 components/ScopeCheckbox.tsx delete mode 100644 lib/types.ts diff --git a/app/page.tsx b/app/page.tsx index 1283536..7efde3b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,6 +4,9 @@ import ClientOnly from "@/components/ClientOnly"; export default function Home() { return ( <main> + <h1 className="mb-5 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl"> + M-OAuth + </h1> <ClientOnly> <InputForm /> </ClientOnly> diff --git a/components/InputForm.tsx b/components/InputForm.tsx index cc402c2..d11be49 100644 --- a/components/InputForm.tsx +++ b/components/InputForm.tsx @@ -1,10 +1,10 @@ -'use client' +"use client"; -import * as z from "zod" -import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" +import * as z from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; -import { Button } from "@/components/ui/button" +import { Button } from "@/components/ui/button"; import { Form, FormControl, @@ -13,36 +13,84 @@ import { FormItem, FormLabel, FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { readScopes, writeScopes, adminScopes } from "@/lib/utils" -import ScopeSection from "./ScopeSection" +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + READ_SCOPES, + WRITE_SCOPES, + ADMIN_READ_SCOPES, + ADMIN_WRITE_SCOPES, +} from "@/lib/utils"; +import ScopeSection from "./ScopeSection"; + +export type MethodType = + | "read" + | "write" + | "follow" + | "crypto" + | "follow" + | "admin" + | "push"; +export interface ScopeInfo { + method: MethodType; + scopes?: string[] | string[][]; + description: string; +} + +const scopesInfo: ScopeInfo[] = [ + { + method: "read", + scopes: READ_SCOPES, + description: "read account's data", + }, + { + method: "write", + scopes: WRITE_SCOPES, + description: "modify account's data", + }, + { + method: "follow", + description: "modify account relationships,deprecated in 3.5.0 and newer.", + }, + { + method: "push", + description: "receive push notifications", + }, + { + method: "admin", + scopes: [ADMIN_READ_SCOPES, ADMIN_WRITE_SCOPES], + description: "read all data on the server", + }, + { + method: "crypto", + description: "use end-to-end encryption", + }, +]; const formSchema = z.object({ instance: z.string().trim(), clientName: z.string().trim(), redirectUris: z.string().url().trim(), scopes: z.string().array().nonempty().optional(), - website: z.string().trim().optional() -}) + website: z.string().trim().optional(), +}); const InputForm = () => { const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { - instance:'https://', - clientName: '', - redirectUris: 'urn:ietf:wg:oauth:2.0:oob', + instance: "https://", + clientName: "", + redirectUris: "", }, - }) + }); function onSubmit(values: z.infer<typeof formSchema>) { - console.log(values) + console.log(values); } return ( <Form {...form}> - <h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl mb-5">M-OAuth</h1> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <FormField control={form.control} @@ -51,7 +99,7 @@ const InputForm = () => { <FormItem> <FormLabel>Instance</FormLabel> <FormControl> - <Input placeholder="mastodon.social" {...field} /> + <Input {...field} /> </FormControl> <FormMessage /> </FormItem> @@ -92,7 +140,13 @@ const InputForm = () => { <FormControl> <Input {...field} /> </FormControl> - <FormDescription>Use <code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-semibold">urn:ietf:wg:oauth:2.0:oob</code> for local tests</FormDescription> + <FormDescription> + Use{" "} + <code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs font-semibold"> + urn:ietf:wg:oauth:2.0:oob + </code>{" "} + for local tests + </FormDescription> <FormMessage /> </FormItem> )} @@ -100,18 +154,27 @@ const InputForm = () => { <FormField control={form.control} name="scopes" - render={({ field }) => ( + render={() => ( <FormItem> <FormLabel>Scopes</FormLabel> <FormControl className="flex flex-col gap-2"> - <div className="flex flex-col gap-2"> - <ScopeSection method="read" scopes={readScopes} /> - <ScopeSection method="write" scopes={writeScopes} /> - <ScopeSection method="admin" scopes={adminScopes} /> - <ScopeSection method="follow" /> - <ScopeSection method="push" /> - <ScopeSection method="crypto" /> - </div> + <FormField + control={form.control} + name="scopes" + render={({ field }) => ( + <FormItem> + <div className="flex flex-col gap-2"> + {scopesInfo.map((info) => ( + <ScopeSection + key={info.method} + info={info} + field={field} + /> + ))} + </div> + </FormItem> + )} + ></FormField> </FormControl> <FormMessage /> </FormItem> @@ -120,7 +183,6 @@ const InputForm = () => { <Button type="submit">Submit</Button> </form> </Form> - ); }; export default InputForm; diff --git a/components/ScopeCheckbox.tsx b/components/ScopeCheckbox.tsx new file mode 100644 index 0000000..a01ad2c --- /dev/null +++ b/components/ScopeCheckbox.tsx @@ -0,0 +1,28 @@ +import { Checkbox } from "@radix-ui/react-checkbox"; +import { MethodType } from "./InputForm"; + +interface ScopeCheckboxProps { + scope: string; + method: MethodType; +} + +const ScopeCheckbox: React.FC<ScopeCheckboxProps> = ({ scope, method }) => { + return ( + <div className={`items-top flex space-x-2 hover:cursor-pointer`}> + <Checkbox id={`${scope}`} /> + <div className="grid gap-1.5 leading-none"> + <label + htmlFor={`${scope}`} + className="text-sm font-medium leading-none hover:cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + > + {method == "admin" && ( + <span className="text-slate-500">{scope.split(":")[1]} : </span> + )} + {scope.split(":").slice(-1)} + </label> + </div> + </div> + ); +}; + +export default ScopeCheckbox; \ No newline at end of file diff --git a/components/ScopeSection.tsx b/components/ScopeSection.tsx index 0f987d3..c0f4cd0 100644 --- a/components/ScopeSection.tsx +++ b/components/ScopeSection.tsx @@ -9,22 +9,21 @@ import { import { Button } from "@/components/ui/button"; import { ChevronsUpDown } from "lucide-react"; -import { ReadScope, AdminScope, WriteScope } from "@/lib/types"; -import { MethodType } from "@/lib/types"; -import { ControllerRenderProps } from "react-hook-form"; - +import { ScopeInfo } from "./InputForm"; +import ScopeCheckbox from "./ScopeCheckbox"; interface ScopeSectionProps { - method: MethodType; - scopes?: ReadScope[] | WriteScope[] | AdminScope[]; + info: ScopeInfo; + field: any; } -const ScopeSection: React.FC<ScopeSectionProps> = ({ method, scopes}) => { +const ScopeSection: React.FC<ScopeSectionProps> = ({ info, field }) => { + const { method, description, scopes } = info; return ( <Collapsible className="flex flex-col rounded-md bg-slate-50 px-4 py-3"> <div className="flex justify-between"> <div className="items-top flex space-x-2 "> - <Checkbox id={method}/> + <Checkbox id={method} /> <div className="grid gap-1.5 leading-none"> <label htmlFor={method} @@ -32,14 +31,7 @@ const ScopeSection: React.FC<ScopeSectionProps> = ({ method, scopes}) => { > {method} </label> - <p className="text-xs text-muted-foreground"> - {method === "read" && "read your account's data"} - {method === "write" && "modify your account's data"} - {method === "admin" && "read data on the server"} - {method === "follow" && "modify account relationships"} - {method === "push" && "receive your push notifications"} - {method === "crypto" && "use end-to-end encryption"} - </p> + <p className="text-xs text-muted-foreground">{description}</p> </div> </div> {scopes && ( @@ -51,28 +43,30 @@ const ScopeSection: React.FC<ScopeSectionProps> = ({ method, scopes}) => { </CollapsibleTrigger> )} </div> - <CollapsibleContent> - {scopes && ( - <div className="grid grid-cols-1 gap-y-4 pb-2 ps-6 pt-5 md:grid-cols-2 "> - {scopes.map((scope) => ( - <div - className="items-top flex space-x-2 hover:cursor-pointer" - key={`${method}:${scope}`} - > - <Checkbox id={`${method + scope}`} /> - <div className="grid gap-1.5 leading-none"> - <label - htmlFor={`${method + scope}`} - className="text-sm font-medium leading-none hover:cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + {scopes && ( + <CollapsibleContent> + <div + className={`grid grid-cols-1 pb-2 ps-6 pt-5 md:grid-cols-2 ${ + method === "admin" ? "" : "gap-2" + }`} + > + {method === "admin" + ? (scopes as string[][]).map((items) => ( + <div + key={`${items}${method}`} + className="flex flex-col gap-2" > - {scope} - </label> - </div> - </div> - ))} + {items.map((item) => ( + <ScopeCheckbox scope={item} key={item} method={method} /> + ))} + </div> + )) + : (scopes as string[]).map((scope) => ( + <ScopeCheckbox scope={scope} key={scope} method={method} /> + ))} </div> - )} - </CollapsibleContent> + </CollapsibleContent> + )} </Collapsible> ); }; diff --git a/lib/types.ts b/lib/types.ts deleted file mode 100644 index 504558b..0000000 --- a/lib/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -export type MethodType = "read" | "write" | "follow" | "push" | 'crypto' | 'admin' - -export type ReadScope = - |"account" - | "blocks" - | "bookmarks" - | "favourites" - | "filters" - | "filters" - | "lists" - | "mutes" - | "notifications" - | "search" - | "statuses"; - -export type WriteScope = Omit<ReadScope, 'search'> - | 'conversations' - | 'media' - | "reports" - -export type AdminScope = - "account" - | "reports" - | "domain_allows" - | "domain_blocks" - | "ip_blocks" - | "email_domain_blocks" - | "canonical_email_blocks"; diff --git a/lib/utils.ts b/lib/utils.ts index 2a756e2..b95e62c 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,45 +1,55 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; -import { ReadScope, WriteScope, AdminScope } from "@/lib/types"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -export const readScopes: ReadScope[] = [ - "account", - "blocks", - "bookmarks", - "favourites", - "filters", - "lists", - "mutes", - "notifications", - "search", - "statuses", +export const READ_SCOPES = [ + "read:accounts", + "read:blocks", + "read:bookmarks", + "read:favourites", + "read:filters", + "read:follows", + "read:lists", + "read:mutes", + "read:notifications", + "read:search", + "read:statuses", ]; -export const writeScopes: WriteScope[] = [ - "account", - "blocks", - "bookmarks", - "favourites", - "filters", - "lists", - "mutes", - "notifications", - "statuses", - "conversations", - "media", - "reports", +export const WRITE_SCOPES = [ + "write:account", + "write:blocks", + "write:bookmarks", + "write:favourites", + "write:filters", + "write:lists", + "write:mutes", + "write:notifications", + "write:statuses", + "write:conversations", + "write:media", + "write:reports", ]; -export const adminScopes: AdminScope[] = [ - "account", - "reports", - "domain_allows", - "domain_blocks", - "ip_blocks", - "email_domain_blocks", - "canonical_email_blocks", +export const ADMIN_READ_SCOPES = [ + "admin:read:account", + "admin:read:reports", + "admin:read:domain_allows", + "admin:read:domain_blocks", + "admin:read:ip_blocks", + "admin:read:email_domain_blocks", + "admin:read:canonical_email_blocks", +]; + +export const ADMIN_WRITE_SCOPES = [ + "admin:write:account", + "admin:write:reports", + "admin:write:domain_allows", + "admin:write:domain_blocks", + "admin:write:ip_blocks", + "admin:write:email_domain_blocks", + "admin:write:canonical_email_blocks", ];