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",
 ];