mirror of
https://github.com/Sevichecc/m-oauth.git
synced 2025-04-30 06:59:29 +08:00
feat: add write adn read scopes to admin
This commit is contained in:
parent
529fe6d4cf
commit
b541573d77
6 changed files with 195 additions and 126 deletions
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
<div className="flex flex-col gap-2">
|
<FormField
|
||||||
<ScopeSection method="read" scopes={readScopes} />
|
control={form.control}
|
||||||
<ScopeSection method="write" scopes={writeScopes} />
|
name="scopes"
|
||||||
<ScopeSection method="admin" scopes={adminScopes} />
|
render={({ field }) => (
|
||||||
<ScopeSection method="follow" />
|
<FormItem>
|
||||||
<ScopeSection method="push" />
|
<div className="flex flex-col gap-2">
|
||||||
<ScopeSection method="crypto" />
|
{scopesInfo.map((info) => (
|
||||||
</div>
|
<ScopeSection
|
||||||
|
key={info.method}
|
||||||
|
info={info}
|
||||||
|
field={field}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</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;
|
||||||
|
|
28
components/ScopeCheckbox.tsx
Normal file
28
components/ScopeCheckbox.tsx
Normal 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;
|
|
@ -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 && (
|
<CollapsibleContent>
|
||||||
<div className="grid grid-cols-1 gap-y-4 pb-2 ps-6 pt-5 md:grid-cols-2 ">
|
<div
|
||||||
{scopes.map((scope) => (
|
className={`grid grid-cols-1 pb-2 ps-6 pt-5 md:grid-cols-2 ${
|
||||||
<div
|
method === "admin" ? "" : "gap-2"
|
||||||
className="items-top flex space-x-2 hover:cursor-pointer"
|
}`}
|
||||||
key={`${method}:${scope}`}
|
>
|
||||||
>
|
{method === "admin"
|
||||||
<Checkbox id={`${method + scope}`} />
|
? (scopes as string[][]).map((items) => (
|
||||||
<div className="grid gap-1.5 leading-none">
|
<div
|
||||||
<label
|
key={`${items}${method}`}
|
||||||
htmlFor={`${method + scope}`}
|
className="flex flex-col gap-2"
|
||||||
className="text-sm font-medium leading-none hover:cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
>
|
||||||
{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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
28
lib/types.ts
28
lib/types.ts
|
@ -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";
|
|
76
lib/utils.ts
76
lib/utils.ts
|
@ -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",
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in a new issue