From e0fa0bbc9be61b27ef16cb3adff2b1c0392b4251 Mon Sep 17 00:00:00 2001 From: Juliet Date: Sat, 6 Dec 2025 09:02:46 +0100 Subject: [PATCH] Oauth scopes (#47) * init * refactor * remove re exports * remove re exports fr * disable blob perm when no create or update * refactor perm localstorage * restyle permission selector * autofocus login --- public/oauth-client-metadata.json | 2 +- src/auth/account.tsx | 190 ++++++++++++++++++++++++++++++ src/auth/login.tsx | 88 ++++++++++++++ src/auth/oauth-config.ts | 13 ++ src/auth/scope-flow.ts | 77 ++++++++++++ src/auth/scope-selector.tsx | 88 ++++++++++++++ src/auth/scope-utils.ts | 53 +++++++++ src/auth/session-manager.ts | 95 +++++++++++++++ src/auth/state.ts | 14 +++ src/components/account.tsx | 170 -------------------------- src/components/create.tsx | 22 ++-- src/components/login.tsx | 143 ---------------------- src/layout.tsx | 6 +- src/views/collection.tsx | 5 +- src/views/record.tsx | 51 ++++---- 15 files changed, 665 insertions(+), 352 deletions(-) create mode 100644 src/auth/account.tsx create mode 100644 src/auth/login.tsx create mode 100644 src/auth/oauth-config.ts create mode 100644 src/auth/scope-flow.ts create mode 100644 src/auth/scope-selector.tsx create mode 100644 src/auth/scope-utils.ts create mode 100644 src/auth/session-manager.ts create mode 100644 src/auth/state.ts delete mode 100644 src/components/account.tsx delete mode 100644 src/components/login.tsx diff --git a/public/oauth-client-metadata.json b/public/oauth-client-metadata.json index f2e8b41..526aa23 100644 --- a/public/oauth-client-metadata.json +++ b/public/oauth-client-metadata.json @@ -4,7 +4,7 @@ "client_uri": "https://pdsls.dev", "logo_uri": "https://pdsls.dev/favicon.ico", "redirect_uris": ["https://pdsls.dev/"], - "scope": "atproto transition:generic", + "scope": "atproto repo:*?action=create repo:*?action=update repo:*?action=delete blob:*/*", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none", diff --git a/src/auth/account.tsx b/src/auth/account.tsx new file mode 100644 index 0000000..7e8fb99 --- /dev/null +++ b/src/auth/account.tsx @@ -0,0 +1,190 @@ +import { Did } from "@atcute/lexicons"; +import { deleteStoredSession, getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; +import { A } from "@solidjs/router"; +import { createSignal, For, onMount, Show } from "solid-js"; +import { createStore, produce } from "solid-js/store"; +import { ActionMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown.jsx"; +import { Modal } from "../components/modal.jsx"; +import { Login } from "./login.jsx"; +import { useOAuthScopeFlow } from "./scope-flow.js"; +import { ScopeSelector } from "./scope-selector.jsx"; +import { parseScopeString } from "./scope-utils.js"; +import { + getAvatar, + loadHandleForSession, + loadSessionsFromStorage, + resumeSession, + retrieveSession, + saveSessionToStorage, +} from "./session-manager.js"; +import { agent, sessions, setAgent, setSessions } from "./state.js"; + +const AccountDropdown = (props: { did: Did; onEditPermissions: (did: Did) => void }) => { + const removeSession = async (did: Did) => { + const currentSession = agent()?.sub; + try { + const session = await getSession(did, { allowStale: true }); + const agent = new OAuthUserAgent(session); + await agent.signOut(); + } catch { + deleteStoredSession(did); + } + setSessions( + produce((accs) => { + delete accs[did]; + }), + ); + saveSessionToStorage(sessions); + if (currentSession === did) setAgent(undefined); + }; + + return ( + + + + props.onEditPermissions(props.did)} + /> + removeSession(props.did)} + /> + + + ); +}; + +export const AccountManager = () => { + const [openManager, setOpenManager] = createSignal(false); + const [avatars, setAvatars] = createStore>(); + const [showingAddAccount, setShowingAddAccount] = createSignal(false); + + const getThumbnailUrl = (avatarUrl: string) => { + return avatarUrl.replace("img/avatar/", "img/avatar_thumbnail/"); + }; + + const scopeFlow = useOAuthScopeFlow({ + beforeRedirect: (account) => resumeSession(account as Did), + }); + + const handleAccountClick = async (did: Did) => { + try { + await resumeSession(did); + } catch { + scopeFlow.initiate(did); + } + }; + + onMount(async () => { + try { + await retrieveSession(); + } catch {} + + const storedSessions = loadSessionsFromStorage(); + if (storedSessions) { + const sessionDids = Object.keys(storedSessions) as Did[]; + sessionDids.forEach(async (did) => { + await loadHandleForSession(did, storedSessions); + }); + sessionDids.forEach(async (did) => { + const avatar = await getAvatar(did); + if (avatar) setAvatars(did, avatar); + }); + } + }); + + return ( + <> + { + setOpenManager(false); + setShowingAddAccount(false); + scopeFlow.cancel(); + }} + > +
+ +
+ Manage accounts +
+
+ + {(did) => ( +
+ setOpenManager(false)} + class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" + > + } + > + + + + + scopeFlow.initiateWithRedirect(accountDid)} + /> +
+ )} +
+
+ +
+ + + setShowingAddAccount(false)} /> + + + + { + scopeFlow.cancel(); + setShowingAddAccount(false); + }} + /> + +
+
+ + + ); +}; diff --git a/src/auth/login.tsx b/src/auth/login.tsx new file mode 100644 index 0000000..a6703a8 --- /dev/null +++ b/src/auth/login.tsx @@ -0,0 +1,88 @@ +import { createSignal, Show } from "solid-js"; +import "./oauth-config"; +import { useOAuthScopeFlow } from "./scope-flow"; +import { ScopeSelector } from "./scope-selector"; + +interface LoginProps { + onCancel?: () => void; +} + +export const Login = (props: LoginProps) => { + const [notice, setNotice] = createSignal(""); + const [loginInput, setLoginInput] = createSignal(""); + + const scopeFlow = useOAuthScopeFlow({ + onError: (e) => setNotice(`${e}`), + onRedirecting: () => { + setNotice(`Contacting your data server...`); + setTimeout(() => setNotice(`Redirecting...`), 0); + }, + }); + + const initiateLogin = (handle: string) => { + setNotice(""); + scopeFlow.initiate(handle); + }; + + const handleCancel = () => { + scopeFlow.cancel(); + setLoginInput(""); + setNotice(""); + props.onCancel?.(); + }; + + return ( +
+ + +
+ +
Add account
+
+
+
e.preventDefault()}> + +
+ + setLoginInput(e.currentTarget.value)} + /> + +
+
+
+ + + + + + +
{notice()}
+
+
+ ); +}; diff --git a/src/auth/oauth-config.ts b/src/auth/oauth-config.ts new file mode 100644 index 0000000..779ca1a --- /dev/null +++ b/src/auth/oauth-config.ts @@ -0,0 +1,13 @@ +import { configureOAuth, defaultIdentityResolver } from "@atcute/oauth-browser-client"; +import { didDocumentResolver, handleResolver } from "../utils/api"; + +configureOAuth({ + metadata: { + client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, + redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL, + }, + identityResolver: defaultIdentityResolver({ + handleResolver: handleResolver, + didDocumentResolver: didDocumentResolver, + }), +}); diff --git a/src/auth/scope-flow.ts b/src/auth/scope-flow.ts new file mode 100644 index 0000000..0d4c929 --- /dev/null +++ b/src/auth/scope-flow.ts @@ -0,0 +1,77 @@ +import { isDid, isHandle } from "@atcute/lexicons/syntax"; +import { createAuthorizationUrl } from "@atcute/oauth-browser-client"; +import { createSignal } from "solid-js"; + +interface UseOAuthScopeFlowOptions { + onError?: (error: unknown) => void; + onRedirecting?: () => void; + beforeRedirect?: (account: string) => Promise; +} + +export const useOAuthScopeFlow = (options: UseOAuthScopeFlowOptions = {}) => { + const [showScopeSelector, setShowScopeSelector] = createSignal(false); + const [pendingAccount, setPendingAccount] = createSignal(""); + const [shouldForceRedirect, setShouldForceRedirect] = createSignal(false); + + const initiate = (account: string) => { + if (!account) return; + setPendingAccount(account); + setShouldForceRedirect(false); + setShowScopeSelector(true); + }; + + const initiateWithRedirect = (account: string) => { + if (!account) return; + setPendingAccount(account); + setShouldForceRedirect(true); + setShowScopeSelector(true); + }; + + const complete = async (scopeString: string, scopeIds: string) => { + try { + const account = pendingAccount(); + + if (options.beforeRedirect && !shouldForceRedirect()) { + try { + await options.beforeRedirect(account); + setShowScopeSelector(false); + return; + } catch {} + } + + localStorage.setItem("pendingScopes", scopeIds); + + options.onRedirecting?.(); + + const authUrl = await createAuthorizationUrl({ + scope: scopeString, + target: + isHandle(account) || isDid(account) ? + { type: "account", identifier: account } + : { type: "pds", serviceUrl: account }, + }); + + await new Promise((resolve) => setTimeout(resolve, 250)); + location.assign(authUrl); + } catch (e) { + console.error(e); + options.onError?.(e); + setShowScopeSelector(false); + } + }; + + const cancel = () => { + setShowScopeSelector(false); + setPendingAccount(""); + setShouldForceRedirect(false); + }; + + return { + showScopeSelector, + pendingAccount, + initiate, + initiateWithRedirect, + complete, + cancel, + }; +}; diff --git a/src/auth/scope-selector.tsx b/src/auth/scope-selector.tsx new file mode 100644 index 0000000..44af221 --- /dev/null +++ b/src/auth/scope-selector.tsx @@ -0,0 +1,88 @@ +import { createSignal, For } from "solid-js"; +import { buildScopeString, GRANULAR_SCOPES, scopeIdsToString } from "./scope-utils"; + +interface ScopeSelectorProps { + onConfirm: (scopeString: string, scopeIds: string) => void; + onCancel: () => void; + initialScopes?: Set; +} + +export const ScopeSelector = (props: ScopeSelectorProps) => { + const [selectedScopes, setSelectedScopes] = createSignal>( + props.initialScopes || new Set(["create", "update", "delete", "blob"]), + ); + + const isBlobDisabled = () => { + const scopes = selectedScopes(); + return !scopes.has("create") && !scopes.has("update"); + }; + + const toggleScope = (scopeId: string) => { + setSelectedScopes((prev) => { + const newSet = new Set(prev); + if (newSet.has(scopeId)) { + newSet.delete(scopeId); + if ( + (scopeId === "create" || scopeId === "update") && + !newSet.has("create") && + !newSet.has("update") + ) { + newSet.delete("blob"); + } + } else { + newSet.add(scopeId); + } + return newSet; + }); + }; + + const handleConfirm = () => { + const scopes = selectedScopes(); + const scopeString = buildScopeString(scopes); + const scopeIds = scopeIdsToString(scopes); + props.onConfirm(scopeString, scopeIds); + }; + + return ( +
+
+ +
Select permissions
+
+
+ + {(scope) => ( +
+ toggleScope(scope.id)} + /> + +
+ )} +
+
+
+ +
+
+ ); +}; diff --git a/src/auth/scope-utils.ts b/src/auth/scope-utils.ts new file mode 100644 index 0000000..3ed5b5e --- /dev/null +++ b/src/auth/scope-utils.ts @@ -0,0 +1,53 @@ +import { agent, sessions } from "./state"; + +export const GRANULAR_SCOPES = [ + { + id: "create", + scope: "repo:*?action=create", + label: "Create records", + }, + { + id: "update", + scope: "repo:*?action=update", + label: "Update records", + }, + { + id: "delete", + scope: "repo:*?action=delete", + label: "Delete records", + }, + { + id: "blob", + scope: "blob:*/*", + label: "Upload blobs", + }, +]; + +export const BASE_SCOPES = ["atproto"]; + +export const buildScopeString = (selected: Set): string => { + const granular = GRANULAR_SCOPES.filter((s) => selected.has(s.id)).map((s) => s.scope); + return [...BASE_SCOPES, ...granular].join(" "); +}; + +export const scopeIdsToString = (scopeIds: Set): string => { + return ["atproto", ...Array.from(scopeIds)].join(","); +}; + +export const parseScopeString = (scopeIdsString: string): Set => { + if (!scopeIdsString) return new Set(); + const ids = scopeIdsString.split(",").filter(Boolean); + return new Set(ids.filter((id) => id !== "atproto")); +}; + +export const hasScope = (grantedScopes: string | undefined, scopeId: string): boolean => { + if (!grantedScopes) return false; + return grantedScopes.split(",").includes(scopeId); +}; + +export const hasUserScope = (scopeId: string): boolean => { + if (!agent()) return false; + const grantedScopes = sessions[agent()!.sub]?.grantedScopes; + if (!grantedScopes) return true; + return hasScope(grantedScopes, scopeId); +}; diff --git a/src/auth/session-manager.ts b/src/auth/session-manager.ts new file mode 100644 index 0000000..0356e00 --- /dev/null +++ b/src/auth/session-manager.ts @@ -0,0 +1,95 @@ +import { Client, CredentialManager } from "@atcute/client"; +import { Did } from "@atcute/lexicons"; +import { + finalizeAuthorization, + getSession, + OAuthUserAgent, + type Session, +} from "@atcute/oauth-browser-client"; +import { resolveDidDoc } from "../utils/api"; +import { Sessions, setAgent, setSessions } from "./state"; + +export const saveSessionToStorage = (sessions: Sessions) => { + localStorage.setItem("sessions", JSON.stringify(sessions)); +}; + +export const loadSessionsFromStorage = (): Sessions | null => { + const localSessions = localStorage.getItem("sessions"); + return localSessions ? JSON.parse(localSessions) : null; +}; + +export const getAvatar = async (did: Did): Promise => { + const rpc = new Client({ + handler: new CredentialManager({ service: "https://public.api.bsky.app" }), + }); + const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } }); + if (res.ok) { + return res.data.avatar; + } + return undefined; +}; + +export const loadHandleForSession = async (did: Did, storedSessions: Sessions) => { + const doc = await resolveDidDoc(did); + const alias = doc.alsoKnownAs?.find((alias) => alias.startsWith("at://")); + if (alias) { + setSessions(did, { + signedIn: storedSessions[did].signedIn, + handle: alias.replace("at://", ""), + grantedScopes: storedSessions[did].grantedScopes, + }); + } +}; + +export const retrieveSession = async (): Promise => { + const init = async (): Promise => { + const params = new URLSearchParams(location.hash.slice(1)); + + if (params.has("state") && (params.has("code") || params.has("error"))) { + history.replaceState(null, "", location.pathname + location.search); + + const auth = await finalizeAuthorization(params); + const did = auth.session.info.sub; + + localStorage.setItem("lastSignedIn", did); + + const grantedScopes = localStorage.getItem("pendingScopes") || "atproto"; + localStorage.removeItem("pendingScopes"); + + const sessions = loadSessionsFromStorage(); + const newSessions: Sessions = sessions || {}; + newSessions[did] = { signedIn: true, grantedScopes }; + saveSessionToStorage(newSessions); + return auth.session; + } else { + const lastSignedIn = localStorage.getItem("lastSignedIn"); + + if (lastSignedIn) { + const sessions = loadSessionsFromStorage(); + const newSessions: Sessions = sessions || {}; + try { + const session = await getSession(lastSignedIn as Did); + const rpc = new Client({ handler: new OAuthUserAgent(session) }); + const res = await rpc.get("com.atproto.server.getSession"); + newSessions[lastSignedIn].signedIn = true; + saveSessionToStorage(newSessions); + if (!res.ok) throw res.data.error; + return session; + } catch (err) { + newSessions[lastSignedIn].signedIn = false; + saveSessionToStorage(newSessions); + throw err; + } + } + } + }; + + const session = await init(); + + if (session) setAgent(new OAuthUserAgent(session)); +}; + +export const resumeSession = async (did: Did): Promise => { + localStorage.setItem("lastSignedIn", did); + await retrieveSession(); +}; diff --git a/src/auth/state.ts b/src/auth/state.ts new file mode 100644 index 0000000..d4f55f3 --- /dev/null +++ b/src/auth/state.ts @@ -0,0 +1,14 @@ +import { OAuthUserAgent } from "@atcute/oauth-browser-client"; +import { createSignal } from "solid-js"; +import { createStore } from "solid-js/store"; + +export type Account = { + signedIn: boolean; + handle?: string; + grantedScopes?: string; +}; + +export type Sessions = Record; + +export const [agent, setAgent] = createSignal(); +export const [sessions, setSessions] = createStore(); diff --git a/src/components/account.tsx b/src/components/account.tsx deleted file mode 100644 index f4f09fc..0000000 --- a/src/components/account.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { Client, CredentialManager } from "@atcute/client"; -import { Did } from "@atcute/lexicons"; -import { - createAuthorizationUrl, - deleteStoredSession, - getSession, - OAuthUserAgent, -} from "@atcute/oauth-browser-client"; -import { A } from "@solidjs/router"; -import { createSignal, For, onMount, Show } from "solid-js"; -import { createStore, produce } from "solid-js/store"; -import { resolveDidDoc } from "../utils/api.js"; -import { ActionMenu, DropdownMenu, MenuProvider, NavMenu } from "./dropdown.jsx"; -import { agent, Login, retrieveSession, Sessions, setAgent } from "./login.jsx"; -import { Modal } from "./modal.jsx"; - -export const [sessions, setSessions] = createStore(); - -const AccountDropdown = (props: { did: Did }) => { - const removeSession = async (did: Did) => { - const currentSession = agent()?.sub; - try { - const session = await getSession(did, { allowStale: true }); - const agent = new OAuthUserAgent(session); - await agent.signOut(); - } catch { - deleteStoredSession(did); - } - setSessions( - produce((accs) => { - delete accs[did]; - }), - ); - localStorage.setItem("sessions", JSON.stringify(sessions)); - if (currentSession === did) setAgent(undefined); - }; - - return ( - - - - removeSession(props.did)} - /> - - - ); -}; - -export const AccountManager = () => { - const [openManager, setOpenManager] = createSignal(false); - const [avatars, setAvatars] = createStore>(); - - onMount(async () => { - try { - await retrieveSession(); - } catch {} - - const localSessions = localStorage.getItem("sessions"); - if (localSessions) { - const storedSessions: Sessions = JSON.parse(localSessions); - const sessionDids = Object.keys(storedSessions) as Did[]; - sessionDids.forEach(async (did) => { - const doc = await resolveDidDoc(did); - const alias = doc.alsoKnownAs?.find((alias) => alias.startsWith("at://")); - if (alias) { - setSessions(did, { - signedIn: storedSessions[did].signedIn, - handle: alias.replace("at://", ""), - }); - } - }); - sessionDids.forEach(async (did) => { - const avatar = await getAvatar(did); - if (avatar) setAvatars(did, avatar); - }); - } - }); - - const resumeSession = async (did: Did) => { - try { - localStorage.setItem("lastSignedIn", did); - await retrieveSession(); - } catch { - const authUrl = await createAuthorizationUrl({ - scope: import.meta.env.VITE_OAUTH_SCOPE, - target: { type: "account", identifier: did }, - }); - - await new Promise((resolve) => setTimeout(resolve, 250)); - - location.assign(authUrl); - } - }; - - const getAvatar = async (did: Did) => { - const rpc = new Client({ - handler: new CredentialManager({ service: "https://public.api.bsky.app" }), - }); - const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } }); - if (res.ok) { - return res.data.avatar; - } - return undefined; - }; - - return ( - <> - setOpenManager(false)}> -
-
- Manage accounts -
-
- - {(did) => ( - - )} - -
- -
-
- - - ); -}; diff --git a/src/components/create.tsx b/src/components/create.tsx index 5756e85..2eb1d6a 100644 --- a/src/components/create.tsx +++ b/src/components/create.tsx @@ -5,8 +5,8 @@ import { getSession, OAuthUserAgent } from "@atcute/oauth-browser-client"; import { remove } from "@mary/exif-rm"; import { useNavigate, useParams } from "@solidjs/router"; import { createEffect, createSignal, For, lazy, onCleanup, Show, Suspense } from "solid-js"; -import { agent } from "../components/login.jsx"; -import { sessions } from "./account.jsx"; +import { hasUserScope } from "../auth/scope-utils"; +import { agent, sessions } from "../auth/state"; import { Button } from "./button.jsx"; import { Modal } from "./modal.jsx"; import { addNotification, removeNotification } from "./notification.jsx"; @@ -436,14 +436,16 @@ export const RecordEditor = (props: { create: boolean; record?: any; refetch?: a
- { - setOpenInsertMenu(false); - blobInput.click(); - }} - /> + + { + setOpenInsertMenu(false); + blobInput.click(); + }} + /> + (); - -type Account = { - signedIn: boolean; - handle?: string; -}; - -export type Sessions = Record; - -const Login = () => { - const [notice, setNotice] = createSignal(""); - const [loginInput, setLoginInput] = createSignal(""); - - const login = async (handle: string) => { - try { - setNotice(""); - if (!handle) return; - setNotice(`Contacting your data server...`); - const authUrl = await createAuthorizationUrl({ - scope: import.meta.env.VITE_OAUTH_SCOPE, - target: - isHandle(handle) || isDid(handle) ? - { type: "account", identifier: handle } - : { type: "pds", serviceUrl: handle }, - }); - - setNotice(`Redirecting...`); - await new Promise((resolve) => setTimeout(resolve, 250)); - - location.assign(authUrl); - } catch (e) { - console.error(e); - setNotice(`${e}`); - } - }; - - return ( -
e.preventDefault()}> - -
- - setLoginInput(e.currentTarget.value)} - /> - -
- -
{notice()}
-
-
- ); -}; - -const retrieveSession = async () => { - const init = async (): Promise => { - const params = new URLSearchParams(location.hash.slice(1)); - - if (params.has("state") && (params.has("code") || params.has("error"))) { - history.replaceState(null, "", location.pathname + location.search); - - const auth = await finalizeAuthorization(params); - const did = auth.session.info.sub; - - localStorage.setItem("lastSignedIn", did); - - const sessions = localStorage.getItem("sessions"); - const newSessions: Sessions = sessions ? JSON.parse(sessions) : { [did]: {} }; - newSessions[did] = { signedIn: true }; - localStorage.setItem("sessions", JSON.stringify(newSessions)); - return auth.session; - } else { - const lastSignedIn = localStorage.getItem("lastSignedIn"); - - if (lastSignedIn) { - const sessions = localStorage.getItem("sessions"); - const newSessions: Sessions = sessions ? JSON.parse(sessions) : {}; - try { - const session = await getSession(lastSignedIn as Did); - const rpc = new Client({ handler: new OAuthUserAgent(session) }); - const res = await rpc.get("com.atproto.server.getSession"); - newSessions[lastSignedIn].signedIn = true; - localStorage.setItem("sessions", JSON.stringify(newSessions)); - if (!res.ok) throw res.data.error; - return session; - } catch (err) { - newSessions[lastSignedIn].signedIn = false; - localStorage.setItem("sessions", JSON.stringify(newSessions)); - throw err; - } - } - } - }; - - const session = await init(); - - if (session) setAgent(new OAuthUserAgent(session)); -}; - -export { Login, retrieveSession }; diff --git a/src/layout.tsx b/src/layout.tsx index 9c52ddd..acbea86 100644 --- a/src/layout.tsx +++ b/src/layout.tsx @@ -2,10 +2,10 @@ import { Handle } from "@atcute/lexicons"; import { Meta, MetaProvider } from "@solidjs/meta"; import { A, RouteSectionProps, useLocation, useNavigate } from "@solidjs/router"; import { createEffect, ErrorBoundary, onMount, Show, Suspense } from "solid-js"; -import { AccountManager } from "./components/account.jsx"; +import { AccountManager } from "./auth/account.jsx"; +import { hasUserScope } from "./auth/scope-utils"; import { RecordEditor } from "./components/create.jsx"; import { DropdownMenu, MenuProvider, MenuSeparator, NavMenu } from "./components/dropdown.jsx"; -import { agent } from "./components/login.jsx"; import { NavBar } from "./components/navbar.jsx"; import { NotificationContainer } from "./components/notification.jsx"; import { Search, SearchButton, showSearch } from "./components/search.jsx"; @@ -131,7 +131,7 @@ const Layout = (props: RouteSectionProps) => { - + diff --git a/src/views/collection.tsx b/src/views/collection.tsx index 0ed32f4..d71b7d5 100644 --- a/src/views/collection.tsx +++ b/src/views/collection.tsx @@ -5,9 +5,10 @@ import * as TID from "@atcute/tid"; import { A, useParams } from "@solidjs/router"; import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js"; import { createStore } from "solid-js/store"; +import { hasUserScope } from "../auth/scope-utils"; +import { agent } from "../auth/state"; import { Button } from "../components/button.jsx"; import { JSONType, JSONValue } from "../components/json.jsx"; -import { agent } from "../components/login.jsx"; import { Modal } from "../components/modal.jsx"; import { addNotification, removeNotification } from "../components/notification.jsx"; import { StickyOverlay } from "../components/sticky.jsx"; @@ -198,7 +199,7 @@ const CollectionView = () => {
- +
{
- - - - - setOpenDelete(false)}> -
-

Delete this record?

-
- - + + + + + + + + setOpenDelete(false)}> +
+

Delete this record?

+
+ + +
-
- + + -- 2.51.2