feat: add frontend components and OpenAPI spec for permission history
- Add OpenAPI endpoints for folder and group permission history - Create reusable PermissionHistory component with Svelte 5 - Integrate history display into FolderEditor (for folder admins) - Integrate history display into GroupEditor (for workspace admins) - Fix ownership issues in folders.rs and granular_acls.rs - Frontend validation passes (npm run check) Note: SQLx query cache update required (cargo sqlx prepare) Co-authored-by: Ruben Fiszel <rubenfiszel@users.noreply.github.com>
This commit is contained in:
@@ -11995,6 +11995,40 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/groups_history/get/{name}:
|
||||
get:
|
||||
summary: get group permission history
|
||||
operationId: getGroupPermissionHistory
|
||||
tags:
|
||||
- group
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
responses:
|
||||
"200":
|
||||
description: group permission history
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
changed_by:
|
||||
type: string
|
||||
changed_at:
|
||||
type: string
|
||||
format: date-time
|
||||
change_type:
|
||||
type: string
|
||||
member_affected:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
/w/{workspace}/folders/list:
|
||||
get:
|
||||
summary: list folders
|
||||
@@ -12258,6 +12292,40 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/folders_history/get/{name}:
|
||||
get:
|
||||
summary: get folder permission history
|
||||
operationId: getFolderPermissionHistory
|
||||
tags:
|
||||
- folder
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
responses:
|
||||
"200":
|
||||
description: folder permission history
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
changed_by:
|
||||
type: string
|
||||
changed_at:
|
||||
type: string
|
||||
format: date-time
|
||||
change_type:
|
||||
type: string
|
||||
owner_affected:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
/workers/list:
|
||||
get:
|
||||
summary: list workers
|
||||
|
||||
@@ -329,6 +329,10 @@ async fn update_folder(
|
||||
|
||||
sqlb.set("edited_at", "now()");
|
||||
|
||||
// Track whether permission-related fields are being updated
|
||||
let owners_changed = ng.owners.is_some();
|
||||
let extra_perms_changed = ng.extra_perms.is_some();
|
||||
|
||||
if !authed.is_admin {
|
||||
let prefixed_username = format!("u/{}", authed.username);
|
||||
if ng.owners.as_ref().is_some_and(|x| {
|
||||
@@ -417,7 +421,8 @@ async fn update_folder(
|
||||
.await?;
|
||||
|
||||
// Log permission changes if owners or extra_perms were updated
|
||||
if ng.owners.is_some() || ng.extra_perms.is_some() {
|
||||
let should_log = owners_changed || extra_perms_changed;
|
||||
if should_log {
|
||||
log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
|
||||
@@ -116,7 +116,7 @@ async fn add_granular_acl(
|
||||
"UPDATE {kind} SET extra_perms = jsonb_set(extra_perms, $1, to_jsonb($2), \
|
||||
true) WHERE {identifier} = $3 AND workspace_id = $4 RETURNING extra_perms"
|
||||
))
|
||||
.bind(vec![owner])
|
||||
.bind(vec![owner.clone()])
|
||||
.bind(write.unwrap_or(false))
|
||||
.bind(path)
|
||||
.bind(&w_id)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import Select from './select/Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import PermissionHistory from './PermissionHistory.svelte'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
@@ -441,4 +442,19 @@
|
||||
{/if}
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
{#if can_write}
|
||||
<PermissionHistory
|
||||
{name}
|
||||
kind="folder"
|
||||
fetchHistory={async (workspace, folderName, page, perPage) => {
|
||||
return await FolderService.getFolderPermissionHistory({
|
||||
workspace,
|
||||
name: folderName,
|
||||
page,
|
||||
perPage
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import { Trash } from 'lucide-svelte'
|
||||
import PermissionHistory from './PermissionHistory.svelte'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
@@ -311,4 +312,19 @@
|
||||
</div>
|
||||
{/if}
|
||||
</Label>
|
||||
|
||||
{#if $userStore?.is_admin}
|
||||
<PermissionHistory
|
||||
{name}
|
||||
kind="group"
|
||||
fetchHistory={async (workspace, groupName, page, perPage) => {
|
||||
return await GroupService.getGroupPermissionHistory({
|
||||
workspace,
|
||||
name: groupName,
|
||||
page,
|
||||
perPage
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
99
frontend/src/lib/components/PermissionHistory.svelte
Normal file
99
frontend/src/lib/components/PermissionHistory.svelte
Normal file
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { untrack } from 'svelte'
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
import Label from './Label.svelte'
|
||||
|
||||
interface PermissionChange {
|
||||
id?: number
|
||||
changed_by?: string
|
||||
changed_at?: string
|
||||
change_type?: string
|
||||
owner_affected?: string | null
|
||||
member_affected?: string | null
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
kind: 'folder' | 'group'
|
||||
fetchHistory: (
|
||||
workspace: string,
|
||||
name: string,
|
||||
page: number,
|
||||
perPage: number
|
||||
) => Promise<PermissionChange[]>
|
||||
}
|
||||
|
||||
let { name, kind, fetchHistory }: Props = $props()
|
||||
let history: PermissionChange[] | undefined = $state(undefined)
|
||||
let loading = $state(false)
|
||||
let page = $state(1)
|
||||
let perPage = $state(10)
|
||||
|
||||
async function loadHistory() {
|
||||
if (!$workspaceStore) return
|
||||
loading = true
|
||||
try {
|
||||
history = await fetchHistory($workspaceStore, name, page, perPage)
|
||||
} catch (e) {
|
||||
console.error('Failed to load permission history:', e)
|
||||
history = []
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
function formatChangeType(changeType: string): string {
|
||||
return changeType
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
$effect.pre(() => {
|
||||
if ($workspaceStore && name) {
|
||||
untrack(() => {
|
||||
loadHistory()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Label label="Permission History">
|
||||
{#if loading || history === undefined}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each new Array(3) as _}
|
||||
<Skeleton layout={[[4], 0.7]} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if history.length === 0}
|
||||
<p class="text-primary text-sm">No permission changes recorded yet</p>
|
||||
{:else}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>Changed By</th>
|
||||
<th>Change Type</th>
|
||||
<th>{kind === 'folder' ? 'Owner Affected' : 'Member Affected'}</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
{#snippet body()}
|
||||
<tbody>
|
||||
{#each history as change}
|
||||
<tr>
|
||||
<td>{change.changed_by ?? '-'}</td>
|
||||
<td>{change.change_type ? formatChangeType(change.change_type) : '-'}</td>
|
||||
<td>{change.owner_affected ?? change.member_affected ?? '-'}</td>
|
||||
<td class="text-xs">{change.changed_at ? formatDate(change.changed_at) : '-'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
{/snippet}
|
||||
</TableCustom>
|
||||
{/if}
|
||||
</Label>
|
||||
Reference in New Issue
Block a user