Add S3 password settings to edit window

This commit is contained in:
2026-09-30 19:09:27 +02:00
parent 4eb0d9bedd
commit 38766bbca9
6 changed files with 89 additions and 24 deletions

View File

@@ -299,6 +299,12 @@ export const path_link = (node: HTMLAnchorElement, data: { nav: FSNavigator, nod
// Override click handler
const click = (e: MouseEvent) => {
// Modified clicks open the link in a new tab or window, let the browser
// handle those
if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) {
return
}
e.preventDefault()
data.nav.navigate(data.node.path, true)
}

View File

@@ -1,7 +1,9 @@
<script lang="ts">
import Button from "layout/Button.svelte";
import ToggleButton from "layout/ToggleButton.svelte";
import type { FSPermissions, NodeOptions } from "lib/FilesystemAPI.svelte";
import PermissionButton from "./PermissionButton.svelte";
import { formatDate } from "util/Formatting";
let {
options = $bindable()
@@ -16,7 +18,7 @@ const add_user = (e: SubmitEvent) => {
if (options.user_permissions === undefined) {
options.user_permissions = {} as {[index: string]: FSPermissions}
}
options.user_permissions[new_user_id] = structuredClone(new_user_perms)
options.user_permissions[new_user_id] = $state.snapshot(new_user_perms)
}
const del_user = (id: string) => {
delete options.user_permissions[id]
@@ -24,14 +26,26 @@ const del_user = (id: string) => {
}
let new_password = $state("")
let new_password_label = $state("")
let new_password_perms = $state(<FSPermissions>{read: true})
const add_password = (e: SubmitEvent) => {
e.preventDefault()
if (options.password_permissions === undefined) {
options.password_permissions = {} as {[index: string]: FSPermissions}
}
options.password_permissions[new_password] = structuredClone(new_password_perms)
options.password_permissions[new_password] = {
...$state.snapshot(new_password_perms),
label: new_password_label,
}
new_password = ""
new_password_label = ""
}
const generate_password = () => {
// 32 hexadecimal characters, which any S3 client accepts as secret key
new_password = crypto.randomUUID().replaceAll("-", "")
}
// Passwords stay hidden until their reveal button is pressed
let revealed: {[password: string]: boolean} = $state({})
const del_password = (pass: string) => {
delete options.password_permissions[pass]
options.password_permissions = options.password_permissions
@@ -69,11 +83,14 @@ const del_password = (pass: string) => {
</p>
<form onsubmit={add_user} class="row">
<input type="text" bind:value={new_user_id} placeholder="Username" class="grow" size="1">
<Button type="submit" icon="add" label="Add"/>
<div class="perms">
<PermissionButton bind:permissions={new_user_perms}/>
</div>
<div class="perms">
<Button type="submit" icon="add" label="Add"/>
</div>
</form>
<hr/>
{#if options.user_permissions !== undefined}
{#each Object.keys(options.user_permissions) as id (id)}
<div class="row">
@@ -81,6 +98,13 @@ const del_password = (pass: string) => {
<div class="grow id">
{id}
</div>
<div class="id">
{#if options.user_permissions[id].last_used}
Used {formatDate(options.user_permissions[id].last_used, true, true, false)}
{:else}
Never used
{/if}
</div>
<div class="perms">
<PermissionButton bind:permissions={options.user_permissions[id]}/>
</div>
@@ -93,23 +117,35 @@ const del_password = (pass: string) => {
<legend>Password permissions</legend>
<p>
Allow users to enter a password to give them access to this directory.
</p>
<p>
<b>This feature is not implemented currently!</b>
Passwords can be used as S3 secret keys, see S3 access below.
</p>
<form onsubmit={add_password} class="row">
<input type="text" bind:value={new_password} placeholder="Password" class="grow" size="1">
<Button type="submit" icon="add" label="Add"/>
<input type="text" bind:value={new_password_label} placeholder="Label" class="grow" maxlength="100" size="1">
<input type="text" bind:value={new_password} placeholder="Password" class="grow" size="1" minlength="8" maxlength="50">
<Button type="button" click={generate_password} icon="key" label="Generate"/>
<div class="perms">
<PermissionButton bind:permissions={new_password_perms}/>
</div>
<div class="perms">
<Button type="submit" icon="add" label="Add"/>
</div>
</form>
<hr/>
{#if options.password_permissions !== undefined}
{#each Object.keys(options.password_permissions) as password (password)}
<div class="row">
<Button click={() => del_password(password)} icon="delete"/>
<input type="text" bind:value={options.password_permissions[password].label} placeholder="Label" class="grow" maxlength="100" size="1">
<ToggleButton bind:on={revealed[password]} icon_on="visibility" icon_off="visibility_off"/>
<div class="grow id">
{password}
{revealed[password] ? password : "••••••••••••••••"}
</div>
<div class="id">
{#if options.password_permissions[password].last_used}
Used {formatDate(options.password_permissions[password].last_used, true, true, false)}
{:else}
Never used
{/if}
</div>
<div class="perms">
<PermissionButton bind:permissions={options.password_permissions[password]}/>

View File

@@ -139,6 +139,7 @@ const save = async (e: SubmitEvent) => {
<SharingOptions
bind:file
bind:options
owner={$nav.permissions.owner}
/>
{:else if tab === "branding"}
<BrandingOptions

View File

@@ -9,9 +9,11 @@ import { user } from "lib/UserStore";
let {
file = $bindable(),
options = $bindable(),
owner = false,
}: {
file?: FSNode;
options: NodeOptions;
owner?: boolean;
} = $props();
let is_shared = $derived(file.is_shared() || options.link_permissions?.read)
@@ -42,6 +44,7 @@ const toggle_example = () => {
}
let share_link = $derived(window.location.protocol+"//"+window.location.host+"/d/"+file.id)
const s3_endpoint = window.location.protocol+"//s3."+window.location.host
</script>
{#if $user.subscription.file_sharing === false}
@@ -69,7 +72,26 @@ let share_link = $derived(window.location.protocol+"//"+window.location.host+"/d
</div>
</fieldset>
<AccessControl options={options}/>
<AccessControl bind:options={options}/>
{#if owner && file.type === "dir" && file.id !== "me"}
<fieldset>
<legend>S3 access</legend>
<p>
This directory can be used as a bucket by backup tools and other
software which supports Amazon S3. The directory ID is the access key,
and the passwords above are the secret keys. Each password's permissions
decide what it can do.
</p>
<table>
<tbody>
<tr><td>Endpoint</td><td>{s3_endpoint}</td><td><CopyButton text={s3_endpoint}>Copy</CopyButton></td></tr>
<tr><td>Bucket and access key</td><td>{file.id}</td><td><CopyButton text={file.id}>Copy</CopyButton></td></tr>
<tr><td>Region</td><td>us-east-1 (any region works)</td><td></td></tr>
</tbody>
</table>
</fieldset>
{/if}
<fieldset>
<legend>Embedding</legend>

View File

@@ -6,7 +6,7 @@ let {
group_first = false,
group_middle = false,
group_last = false,
highlight = true,
highlight = false,
flat = false,
action,
children,
@@ -35,16 +35,16 @@ const click = (e: MouseEvent) => {
onclick={click}
type="button"
class="button"
class:button_highlight={on && highlight}
class:button_highlight={highlight}
class:group_first
class:group_middle
class:group_last
class:flat
>
{#if on}
<i class="icon">{icon_on}</i>
<i class="icon green">{icon_on}</i>
{:else}
<i class="icon">{icon_off}</i>
<i class="icon red">{icon_off}</i>
{/if}
{@render children?.()}
</button>
@@ -53,4 +53,10 @@ const click = (e: MouseEvent) => {
.button {
flex: 0 0 content;
}
.green {
color: var(--highlight_color);
}
.red {
color: var(--danger_color);
}
</style>

View File

@@ -104,6 +104,10 @@ export type FSPermissions = {
read: boolean,
write: boolean,
delete: boolean,
// Only used for passwords
label?: string,
last_used?: string,
}
export type FSContext = {
@@ -289,16 +293,6 @@ export const fs_timeseries = async (path: string, start: Date, end: Date, interv
) as NodeTimeSeries
}
export const fs_import = async (parent_dir_path = "", filelist: Array<string>) => {
const form = new FormData()
form.append("action", "import")
form.append("files", JSON.stringify(filelist))
return await fs_check_response(
await fetch(fs_path_url(parent_dir_path), { method: "POST", body: form })
) as GenericResponse
}
// Utility functions
// =================