diff --git a/src/auth/account.tsx b/src/auth/account.tsx index b97159f..206afe9 100644 --- a/src/auth/account.tsx +++ b/src/auth/account.tsx @@ -188,6 +188,8 @@ export const AccountManager = () => { { const scopeFlow = useOAuthScopeFlow({ onError: (e) => setNotice(`${e}`), - onRedirecting: () => { - setNotice(`Contacting your data server...`); - setTimeout(() => setNotice(`Redirecting...`), 0); - }, }); const initiateLogin = (handle: string) => { diff --git a/src/auth/scope-selector.tsx b/src/auth/scope-selector.tsx index 42b8bb4..bd17de9 100644 --- a/src/auth/scope-selector.tsx +++ b/src/auth/scope-selector.tsx @@ -1,59 +1,136 @@ -import { createSignal, For, Show } from "solid-js"; +import { createSignal, Show } from "solid-js"; import { AlphaBadge } from "../components/alpha-badge.jsx"; import { buildScopeString, - GRANULAR_SCOPES, SPACE_MANAGE_RECORDS_SCOPE_ID, SPACE_READ_SCOPE_ID, } from "./scope-utils"; interface ScopeSelectorProps { - onConfirm: (scopeString: string) => void; + onConfirm: (scopeString: string) => void | Promise; onCancel: () => void; initialScopes?: Set; + title?: string; + confirmLabel?: string; } +const PermissionRow = (props: { + label: string; + description?: string; + checked: boolean; + onClick: () => void; +}) => ( + +); + export const ScopeSelector = (props: ScopeSelectorProps) => { - const [selectedScopes, setSelectedScopes] = createSignal>( + const initialScopes = () => { // Space access is deliberately opt-in while the protocol is in alpha. - props.initialScopes || new Set(["create", "update", "delete", "blob"]), - ); - - const isBlobDisabled = () => { - const scopes = selectedScopes(); - return ( - !scopes.has("create") && !scopes.has("update") && !scopes.has(SPACE_MANAGE_RECORDS_SCOPE_ID) - ); + const scopes = new Set(props.initialScopes || ["create", "update", "delete"]); + scopes.delete("blob"); + if (scopes.has("create") || scopes.has("update")) { + scopes.add("create"); + scopes.add("update"); + } + if (scopes.has(SPACE_MANAGE_RECORDS_SCOPE_ID)) scopes.add(SPACE_READ_SCOPE_ID); + return scopes; }; + const initial = initialScopes(); + const [selectedScopes, setSelectedScopes] = createSignal(initial); + const [submitting, setSubmitting] = createSignal(false); const toggleScope = (scopeId: string) => { setSelectedScopes((prev) => { const newSet = new Set(prev); if (newSet.has(scopeId)) { newSet.delete(scopeId); - if (scopeId === SPACE_READ_SCOPE_ID) { - newSet.delete(SPACE_MANAGE_RECORDS_SCOPE_ID); - } - if ( - !newSet.has("create") && - !newSet.has("update") && - !newSet.has(SPACE_MANAGE_RECORDS_SCOPE_ID) - ) { - newSet.delete("blob"); - } } else { newSet.add(scopeId); - if (scopeId === SPACE_MANAGE_RECORDS_SCOPE_ID) { - newSet.add(SPACE_READ_SCOPE_ID); - } } return newSet; }); }; - const handleConfirm = () => { - props.onConfirm(buildScopeString(selectedScopes())); + const hasCreate = () => selectedScopes().has("create"); + const hasUpdate = () => selectedScopes().has("update"); + const hasWrite = () => hasCreate() && hasUpdate(); + + const toggleWrite = () => { + setSelectedScopes((prev) => { + const newSet = new Set(prev); + if (newSet.has("create") && newSet.has("update")) { + newSet.delete("create"); + newSet.delete("update"); + } else { + newSet.add("create"); + newSet.add("update"); + } + return newSet; + }); + }; + + const toggleSpaceRead = () => { + setSelectedScopes((prev) => { + const newSet = new Set(prev); + if (newSet.has(SPACE_READ_SCOPE_ID)) { + newSet.delete(SPACE_READ_SCOPE_ID); + newSet.delete(SPACE_MANAGE_RECORDS_SCOPE_ID); + } else { + newSet.add(SPACE_READ_SCOPE_ID); + } + return newSet; + }); + }; + + const toggleSpaceEdit = () => { + setSelectedScopes((prev) => { + const newSet = new Set(prev); + if (newSet.has(SPACE_MANAGE_RECORDS_SCOPE_ID)) { + newSet.delete(SPACE_MANAGE_RECORDS_SCOPE_ID); + } else { + newSet.add(SPACE_READ_SCOPE_ID); + newSet.add(SPACE_MANAGE_RECORDS_SCOPE_ID); + } + return newSet; + }); + }; + + const handleConfirm = async () => { + if (submitting()) return; + setSubmitting(true); + try { + await props.onConfirm(buildScopeString(selectedScopes())); + } catch { + setSubmitting(false); + } }; return ( @@ -65,47 +142,61 @@ export const ScopeSelector = (props: ScopeSelectorProps) => { > -
Select permissions
+
{props.title || "Select permissions"}
-
- - {(scope) => { - const isSelected = () => selectedScopes().has(scope.id); - const isDisabled = () => scope.id === "blob" && isBlobDisabled(); +
+
+
+ Repository +
+
+ + toggleScope("delete")} + /> +
+
- return ( - - ); - }} - +
+
+ + Spaces + + +
+
+ + +
+
); diff --git a/src/auth/scope-utils.ts b/src/auth/scope-utils.ts index 7e8cb2f..b20e167 100644 --- a/src/auth/scope-utils.ts +++ b/src/auth/scope-utils.ts @@ -12,12 +12,12 @@ export const GRANULAR_SCOPES = [ { id: "create", scope: "repo:*?action=create", - label: "Create records", + label: "Create and edit records", }, { id: "update", scope: "repo:*?action=update", - label: "Update records", + label: "Create and edit records", }, { id: "delete", @@ -32,13 +32,13 @@ export const GRANULAR_SCOPES = [ { id: SPACE_READ_SCOPE_ID, scope: SPACE_READ_SCOPE, - label: "Read Space records", + label: "View non-public records", alpha: true, }, { id: SPACE_MANAGE_RECORDS_SCOPE_ID, scope: SPACE_MANAGE_RECORDS_SCOPE, - label: "Manage Space records", + label: "Write non-public records", alpha: true, }, ] as const; @@ -48,7 +48,14 @@ export type ScopeId = (typeof GRANULAR_SCOPES)[number]["id"]; const BASE_SCOPES = ["atproto"]; export const buildScopeString = (selected: Set): string => { - const granular = GRANULAR_SCOPES.filter((s) => selected.has(s.id)).map((s) => s.scope); + const needsSpaceReadScope = selected.has(SPACE_MANAGE_RECORDS_SCOPE_ID); + const needsBlobScope = + selected.has("create") || selected.has("update") || selected.has(SPACE_MANAGE_RECORDS_SCOPE_ID); + const granular = GRANULAR_SCOPES.filter(({ id }) => { + if (id === "blob") return needsBlobScope; + if (id === SPACE_READ_SCOPE_ID) return needsSpaceReadScope || selected.has(id); + return selected.has(id); + }).map(({ scope }) => scope); return [...BASE_SCOPES, ...granular].join(" "); }; diff --git a/src/components/create/confirm-submit.tsx b/src/components/create/confirm-submit.tsx index 381ef01..db55597 100644 --- a/src/components/create/confirm-submit.tsx +++ b/src/components/create/confirm-submit.tsx @@ -59,7 +59,7 @@ export const ConfirmSubmit = (props: { onChange={(e) => props.setRecreate(e.currentTarget.checked)} class="h-3.5 w-3.5 accent-blue-500" /> - Recreate{!hasUserScope("create") ? " (create permission needed)" : ""} + Recreate{!hasUserScope("create") ? " (create and edit permission needed)" : ""}

Delete the existing record and create a new one with the same record key. diff --git a/src/components/permission-prompt.tsx b/src/components/permission-prompt.tsx index 68f0afd..9506089 100644 --- a/src/components/permission-prompt.tsx +++ b/src/components/permission-prompt.tsx @@ -21,7 +21,7 @@ export const PermissionPromptContainer = () => { const scopeLabel = () => { const scope = GRANULAR_SCOPES.find((s) => s.id === displayedScope()); - return scope?.label.toLowerCase() || displayedScope(); + return scope?.label || displayedScope(); }; const handleEditPermissions = () => { @@ -41,7 +41,7 @@ export const PermissionPromptContainer = () => { >

Permission required

- You need the "{scopeLabel()}" permission to perform this action. + Enable “{scopeLabel()}” in permissions to perform this action.

diff --git a/src/views/spaces/index.tsx b/src/views/spaces/index.tsx index 2296967..a1b33e7 100644 --- a/src/views/spaces/index.tsx +++ b/src/views/spaces/index.tsx @@ -57,7 +57,7 @@ const PermissionPrompt = () => {

Space permission required

- Re-authorize this account and enable “Read Space records”. + Re-authorize this account and enable “View non-public records”.