feat: add write adn read scopes to admin

This commit is contained in:
SevicheCC 2023-06-05 16:08:57 +08:00
parent 529fe6d4cf
commit b541573d77
Signed by: SevicheCC
GPG key ID: C577000000000000
6 changed files with 195 additions and 126 deletions

View file

@ -4,6 +4,9 @@ import ClientOnly from "@/components/ClientOnly";
export default function Home() { export default function Home() {
return ( return (
<main> <main>
<h1 className="mb-5 scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
M-OAuth
</h1>
<ClientOnly> <ClientOnly>
<InputForm /> <InputForm />
</ClientOnly> </ClientOnly>

View file

@ -1,10 +1,10 @@
'use client' "use client";
import * as z from "zod" import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
@ -13,36 +13,84 @@ import {
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form";
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input";
import { readScopes, writeScopes, adminScopes } from "@/lib/utils" import {
import ScopeSection from "./ScopeSection" 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({ const formSchema = z.object({
instance: z.string().trim(), instance: z.string().trim(),
clientName: z.string().trim(), clientName: z.string().trim(),
redirectUris: z.string().url().trim(), redirectUris: z.string().url().trim(),
scopes: z.string().array().nonempty().optional(), scopes: z.string().array().nonempty().optional(),
website: z.string().trim().optional() website: z.string().trim().optional(),
}) });
const InputForm = () => { const InputForm = () => {
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
instance:'https://', instance: "https://",
clientName: '', clientName: "",
redirectUris: 'urn:ietf:wg:oauth:2.0:oob', redirectUris: "",
}, },
}) });
function onSubmit(values: z.infer<typeof formSchema>) { function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values) console.log(values);
} }
return ( return (
<Form {...form}> <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"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField <FormField
control={form.control} control={form.control}
@ -51,7 +99,7 @@ const InputForm = () => {
<FormItem> <FormItem>
<FormLabel>Instance</FormLabel> <FormLabel>Instance</FormLabel>
<FormControl> <FormControl>
<Input placeholder="mastodon.social" {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -92,7 +140,13 @@ const InputForm = () => {
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </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 /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -100,18 +154,27 @@ const InputForm = () => {
<FormField <FormField
control={form.control} control={form.control}
name="scopes" name="scopes"
render={({ field }) => ( render={() => (
<FormItem> <FormItem>
<FormLabel>Scopes</FormLabel> <FormLabel>Scopes</FormLabel>
<FormControl className="flex flex-col gap-2"> <FormControl className="flex flex-col gap-2">
<FormField
control={form.control}
name="scopes"
render={({ field }) => (
<FormItem>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<ScopeSection method="read" scopes={readScopes} /> {scopesInfo.map((info) => (
<ScopeSection method="write" scopes={writeScopes} /> <ScopeSection
<ScopeSection method="admin" scopes={adminScopes} /> key={info.method}
<ScopeSection method="follow" /> info={info}
<ScopeSection method="push" /> field={field}
<ScopeSection method="crypto" /> />
))}
</div> </div>
</FormItem>
)}
></FormField>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -120,7 +183,6 @@ const InputForm = () => {
<Button type="submit">Submit</Button> <Button type="submit">Submit</Button>
</form> </form>
</Form> </Form>
); );
}; };
export default InputForm; export default InputForm;

View file

@ -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;

View file

@ -9,22 +9,21 @@ import {
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ChevronsUpDown } from "lucide-react"; import { ChevronsUpDown } from "lucide-react";
import { ReadScope, AdminScope, WriteScope } from "@/lib/types"; import { ScopeInfo } from "./InputForm";
import { MethodType } from "@/lib/types"; import ScopeCheckbox from "./ScopeCheckbox";
import { ControllerRenderProps } from "react-hook-form";
interface ScopeSectionProps { interface ScopeSectionProps {
method: MethodType; info: ScopeInfo;
scopes?: ReadScope[] | WriteScope[] | AdminScope[]; field: any;
} }
const ScopeSection: React.FC<ScopeSectionProps> = ({ method, scopes}) => { const ScopeSection: React.FC<ScopeSectionProps> = ({ info, field }) => {
const { method, description, scopes } = info;
return ( return (
<Collapsible className="flex flex-col rounded-md bg-slate-50 px-4 py-3"> <Collapsible className="flex flex-col rounded-md bg-slate-50 px-4 py-3">
<div className="flex justify-between"> <div className="flex justify-between">
<div className="items-top flex space-x-2 "> <div className="items-top flex space-x-2 ">
<Checkbox id={method}/> <Checkbox id={method} />
<div className="grid gap-1.5 leading-none"> <div className="grid gap-1.5 leading-none">
<label <label
htmlFor={method} htmlFor={method}
@ -32,14 +31,7 @@ const ScopeSection: React.FC<ScopeSectionProps> = ({ method, scopes}) => {
> >
{method} {method}
</label> </label>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{description}</p>
{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>
</div> </div>
</div> </div>
{scopes && ( {scopes && (
@ -51,28 +43,30 @@ const ScopeSection: React.FC<ScopeSectionProps> = ({ method, scopes}) => {
</CollapsibleTrigger> </CollapsibleTrigger>
)} )}
</div> </div>
<CollapsibleContent>
{scopes && ( {scopes && (
<div className="grid grid-cols-1 gap-y-4 pb-2 ps-6 pt-5 md:grid-cols-2 "> <CollapsibleContent>
{scopes.map((scope) => (
<div <div
className="items-top flex space-x-2 hover:cursor-pointer" className={`grid grid-cols-1 pb-2 ps-6 pt-5 md:grid-cols-2 ${
key={`${method}:${scope}`} method === "admin" ? "" : "gap-2"
}`}
> >
<Checkbox id={`${method + scope}`} /> {method === "admin"
<div className="grid gap-1.5 leading-none"> ? (scopes as string[][]).map((items) => (
<label <div
htmlFor={`${method + scope}`} key={`${items}${method}`}
className="text-sm font-medium leading-none hover:cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className="flex flex-col gap-2"
> >
{scope} {items.map((item) => (
</label> <ScopeCheckbox scope={item} key={item} method={method} />
</div> ))}
</div> </div>
))
: (scopes as string[]).map((scope) => (
<ScopeCheckbox scope={scope} key={scope} method={method} />
))} ))}
</div> </div>
)}
</CollapsibleContent> </CollapsibleContent>
)}
</Collapsible> </Collapsible>
); );
}; };

View file

@ -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";

View file

@ -1,45 +1,55 @@
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { ReadScope, WriteScope, AdminScope } from "@/lib/types";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
export const readScopes: ReadScope[] = [ export const READ_SCOPES = [
"account", "read:accounts",
"blocks", "read:blocks",
"bookmarks", "read:bookmarks",
"favourites", "read:favourites",
"filters", "read:filters",
"lists", "read:follows",
"mutes", "read:lists",
"notifications", "read:mutes",
"search", "read:notifications",
"statuses", "read:search",
"read:statuses",
]; ];
export const writeScopes: WriteScope[] = [ export const WRITE_SCOPES = [
"account", "write:account",
"blocks", "write:blocks",
"bookmarks", "write:bookmarks",
"favourites", "write:favourites",
"filters", "write:filters",
"lists", "write:lists",
"mutes", "write:mutes",
"notifications", "write:notifications",
"statuses", "write:statuses",
"conversations", "write:conversations",
"media", "write:media",
"reports", "write:reports",
]; ];
export const adminScopes: AdminScope[] = [ export const ADMIN_READ_SCOPES = [
"account", "admin:read:account",
"reports", "admin:read:reports",
"domain_allows", "admin:read:domain_allows",
"domain_blocks", "admin:read:domain_blocks",
"ip_blocks", "admin:read:ip_blocks",
"email_domain_blocks", "admin:read:email_domain_blocks",
"canonical_email_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",
]; ];