feat(repo): hide write UI for users without push access
Fetch the viewer's push permission via GET /repos/{owner}/{repo} when
entering a repo and gate every write affordance behind it: edit and
image-upload buttons in StackedNote, new-fleeting-note and YouTube
buttons in FleetingNotes, and interactive checkboxes in TodoNotes
(rendered disabled when read-only). Anonymous viewers get the same
treatment because the permissions field is absent without auth, which
collapses to canPush = false.
Previously every write button was visible regardless of role, so
read-role collaborators and anonymous viewers could enter edit mode and
type before the save failed with 401/403.
This commit is contained in:
56
src/modules/repo/services/repo.spec.ts
Normal file
56
src/modules/repo/services/repo.spec.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
const { reposGet } = vi.hoisted(() => ({ reposGet: vi.fn() }))
|
||||
|
||||
vi.mock("@/modules/repo/services/octo", () => ({
|
||||
getOctokit: vi.fn().mockResolvedValue({
|
||||
repos: { get: reposGet }
|
||||
}),
|
||||
runWithAuthRetry: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock("@/hooks/useMarkdown.hook", () => ({
|
||||
markdownBuilder: () => ({ render: (s: string) => s })
|
||||
}))
|
||||
|
||||
vi.mock("@/modules/note/cache/prepareNoteCache", () => ({
|
||||
prepareNoteCache: () => ({
|
||||
getCachedNote: vi.fn().mockResolvedValue({ note: null }),
|
||||
saveCacheNote: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
import { getRepoPermission } from "./repo"
|
||||
|
||||
describe("getRepoPermission", () => {
|
||||
beforeEach(() => {
|
||||
reposGet.mockReset()
|
||||
})
|
||||
|
||||
it("returns true when permissions.push is true", async () => {
|
||||
reposGet.mockResolvedValue({
|
||||
data: { permissions: { push: true } }
|
||||
})
|
||||
await expect(getRepoPermission("owner", "repo")).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it("returns false when permissions.push is false", async () => {
|
||||
reposGet.mockResolvedValue({
|
||||
data: { permissions: { push: false } }
|
||||
})
|
||||
await expect(getRepoPermission("owner", "repo")).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it("returns false when permissions is missing (anonymous request)", async () => {
|
||||
reposGet.mockResolvedValue({
|
||||
data: {}
|
||||
})
|
||||
await expect(getRepoPermission("owner", "repo")).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it("returns false when owner or repo is empty", async () => {
|
||||
await expect(getRepoPermission("", "repo")).resolves.toBe(false)
|
||||
await expect(getRepoPermission("owner", "")).resolves.toBe(false)
|
||||
expect(reposGet).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,18 @@ import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
|
||||
import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
|
||||
import { getOctokit, runWithAuthRetry } from "@/modules/repo/services/octo"
|
||||
|
||||
export const getRepoPermission = async (
|
||||
owner: string,
|
||||
repo: string
|
||||
): Promise<boolean> => {
|
||||
if (!owner || !repo) {
|
||||
return false
|
||||
}
|
||||
const octokit = await getOctokit()
|
||||
const { data } = await octokit.repos.get({ owner, repo })
|
||||
return data.permissions?.push ?? false
|
||||
}
|
||||
|
||||
export const getFiles = async (
|
||||
owner: string,
|
||||
repo: string
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getCachedMainReadme,
|
||||
getFiles,
|
||||
getMainReadme,
|
||||
getRepoPermission,
|
||||
getUserSettingsContent
|
||||
} from "@/modules/repo/services/repo"
|
||||
import { refreshToken } from "@/modules/user/service/signIn"
|
||||
@@ -24,6 +25,7 @@ interface State {
|
||||
userSettings?: UserSettings | null
|
||||
needToLogin: boolean
|
||||
loadError: RepoLoadError
|
||||
canPush: boolean
|
||||
_requestId: number
|
||||
}
|
||||
|
||||
@@ -46,6 +48,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
||||
userSettings: undefined,
|
||||
needToLogin: false,
|
||||
loadError: null,
|
||||
canPush: false,
|
||||
_requestId: 0
|
||||
}),
|
||||
actions: {
|
||||
@@ -78,6 +81,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
||||
this.user = user
|
||||
this.repo = repo
|
||||
this.loadError = null
|
||||
this.canPush = false
|
||||
|
||||
let lsLayout: Partial<UserSettings> = {}
|
||||
try {
|
||||
@@ -120,6 +124,17 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
||||
|
||||
if (requestId !== this._requestId) return
|
||||
|
||||
getRepoPermission(user, repo)
|
||||
.then((canPush) => {
|
||||
if (requestId !== this._requestId) return
|
||||
this.canPush = canPush
|
||||
})
|
||||
.catch((error) => {
|
||||
if (requestId !== this._requestId) return
|
||||
console.warn("getRepoPermission failed", error)
|
||||
this.canPush = false
|
||||
})
|
||||
|
||||
getFiles(user, repo)
|
||||
.then(async (files) => {
|
||||
if (requestId !== this._requestId) return
|
||||
@@ -261,6 +276,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
||||
resetFiles() {
|
||||
this.files = []
|
||||
this.readme = null
|
||||
this.canPush = false
|
||||
},
|
||||
setFontFamily(fontFamily: string) {
|
||||
if (!this.userSettings) {
|
||||
|
||||
Reference in New Issue
Block a user