feat(v3-auth): modernize hosted file access with simplified bypass pattern
- Roll out platform-wide standard for unauthenticated binary access using '?key=[account_id]' query parameter. - Update API helpers (get, post, patch) to recognize 'key' bypass and strip account context headers accordingly. - Refactor IDAA Bulletin Board to restore inline image rendering and edit-mode previews. - Modernize Events Launcher (Layout, Sync, Session View) to use V3 Action URLs with verified auth. - Update HTML generators in 'ae_utils.ts' to support the new authenticated URL structure. - Harden 'ae_comp__event_file_obj_tbl' CSV export and clipboard links with V3 standard patterns.
This commit is contained in:
@@ -20,6 +20,8 @@
|
||||
slct_trigger
|
||||
} from '$lib/stores/ae_stores';
|
||||
|
||||
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
|
||||
|
||||
// Exports
|
||||
|
||||
// export let input_name = 'file_list';
|
||||
@@ -178,10 +180,7 @@
|
||||
|
||||
{#each Object.entries(hosted_file_obj_kv) as [hosted_file_id, hosted_file_obj]}
|
||||
<div class="border border-gray-300 rounded-lg p-2 m-2">
|
||||
<!-- {#if $ae_sess.files[hosted_file_id].upload_complete}
|
||||
<a href="/hosted_file/{hosted_file_id}/download?x_no_account_id_token=direct-download" download={$ae_sess.files[hosted_file_id].new_filename} class="ae_btn btn_lg btn_primary"><span class="fas fa-download"></span> Ready to Download</a>
|
||||
{/if} -->
|
||||
|
||||
<!-- Download Button (Standardized) -->
|
||||
<div class="flex flex-row flex-wrap gap-1 justify-center items-center w-full">
|
||||
<!-- Remove from uploaded file kv list -->
|
||||
<button
|
||||
@@ -211,52 +210,17 @@
|
||||
</button>
|
||||
|
||||
<!-- Download the file -->
|
||||
<button
|
||||
type="button"
|
||||
disabled={!$ae_loc.trusted_access}
|
||||
onclick={() => {
|
||||
ae_promises[hosted_file_id] = api.download_hosted_file({
|
||||
api_cfg: $ae_api,
|
||||
hosted_file_id: hosted_file_id,
|
||||
return_file: true,
|
||||
filename: hosted_file_obj.filename,
|
||||
auto_download: true,
|
||||
log_lvl: 0
|
||||
});
|
||||
|
||||
}}
|
||||
class:hidden={!$ae_loc.edit_mode}
|
||||
class="novi_btn btn btn-sm lg:btn-md preset-tonal-primary hover:preset-filled-primary-500 min-w-72 lg:min-w-96"
|
||||
title={`Download this file:\n${hosted_file_obj.filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}... Hosted ID: ${hosted_file_obj.hosted_file_id_random}`}
|
||||
>
|
||||
{#await ae_promises[hosted_file_id]}
|
||||
<span class="fas fa-spinner fa-spin mx-1"></span>
|
||||
{#if submit_status == 'clipping'}
|
||||
<span class="">Clipping</span>
|
||||
{:else}
|
||||
<span class="">
|
||||
Downloading
|
||||
{#if $ae_sess.api_download_kv[hosted_file_id]}
|
||||
{$ae_sess.api_download_kv[hosted_file_id].percent_completed}%
|
||||
{/if}
|
||||
:
|
||||
</span>
|
||||
{/if}
|
||||
{:then}
|
||||
<span
|
||||
class="fas fa-{ae_util.file_extension_icon(
|
||||
hosted_file_obj?.file_extension
|
||||
)}"
|
||||
></span>
|
||||
{/await}
|
||||
|
||||
<span class="grow">
|
||||
{ae_util.shorten_filename({
|
||||
filename: hosted_file_obj?.filename,
|
||||
max_length: 30
|
||||
})}
|
||||
</span>
|
||||
</button>
|
||||
<div class:hidden={!$ae_loc.edit_mode}>
|
||||
<AE_Comp_Hosted_Files_Download_Button
|
||||
hosted_file_id={hosted_file_id}
|
||||
hosted_file_obj={hosted_file_obj}
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
classes="novi_btn btn-sm lg:btn-md min-w-72 lg:min-w-96 !justify-start"
|
||||
show_divider={true}
|
||||
max_filename={30}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
>{ae_util.shorten_filename({
|
||||
filename: hosted_file_obj?.filename,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
// Imports
|
||||
// Import components and elements
|
||||
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
|
||||
|
||||
// Import storage, functions, and libraries
|
||||
import type { key_val } from '$lib/stores/ae_stores';
|
||||
@@ -47,44 +48,13 @@
|
||||
<h3 class="h3">{Object.entries($ae_loc.files).length}× files clipped</h3>
|
||||
<div class="{class_li_default} {class_li} ">
|
||||
{#each Object.entries(video_clip_file_kv) as [hosted_file_id, hosted_file_obj]}
|
||||
<button
|
||||
type="button"
|
||||
disabled={!$ae_loc.trusted_access}
|
||||
onclick={() => {
|
||||
ae_promises[hosted_file_id] = api.download_hosted_file({
|
||||
api_cfg: $ae_api,
|
||||
hosted_file_id: hosted_file_id,
|
||||
return_file: true,
|
||||
filename: hosted_file_obj.filename,
|
||||
auto_download: true,
|
||||
log_lvl: log_lvl
|
||||
});
|
||||
|
||||
}}
|
||||
class="novi_btn btn btn-sm lg:btn-md preset-tonal-primary hover:preset-filled-primary-500 min-w-72 lg:min-w-96"
|
||||
title={`Download this file:\n${hosted_file_obj.filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}... Hosted ID: ${hosted_file_obj.hosted_file_id}`}
|
||||
>
|
||||
{#await ae_promises[hosted_file_id]}
|
||||
<span class="fas fa-spinner fa-spin mx-1"></span>
|
||||
<span class="">
|
||||
Downloading
|
||||
{#if $ae_sess.api_download_kv[hosted_file_id]}
|
||||
{$ae_sess.api_download_kv[hosted_file_id].percent_completed}%
|
||||
{/if}
|
||||
:
|
||||
</span>
|
||||
{:then}
|
||||
<span class="fas fa-{ae_util.file_extension_icon(hosted_file_obj?.file_extension)}"
|
||||
></span>
|
||||
{/await}
|
||||
|
||||
<span class="grow">
|
||||
{ae_util.shorten_filename({ filename: hosted_file_obj?.filename, max_length: 30 })}
|
||||
</span>
|
||||
|
||||
<span class="shrink">
|
||||
{ae_util.format_bytes(hosted_file_obj?.size)}
|
||||
</span>
|
||||
</button>
|
||||
<AE_Comp_Hosted_Files_Download_Button
|
||||
{hosted_file_id}
|
||||
{hosted_file_obj}
|
||||
max_filename={30}
|
||||
classes="btn btn-sm lg:btn-md preset-tonal-primary hover:preset-filled-primary-500 min-w-72 lg:min-w-96"
|
||||
linked_to_type={link_to_type}
|
||||
linked_to_id={link_to_id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
show_divider?: boolean;
|
||||
show_direct_download?: boolean;
|
||||
classes?: string;
|
||||
click?: () => void | Promise<any>;
|
||||
label?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
@@ -50,6 +51,7 @@
|
||||
show_divider = true,
|
||||
show_direct_download = false,
|
||||
classes = '',
|
||||
click,
|
||||
label
|
||||
}: Props = $props();
|
||||
|
||||
@@ -166,8 +168,50 @@
|
||||
// Legacy endpoints often expect integer IDs and will return 404 for string IDs.
|
||||
const file_id = hosted_file_obj.event_file_id || hosted_file_obj.hosted_file_id || hosted_file_id;
|
||||
const obj_type_path = hosted_file_obj.event_file_id ? 'event_file' : 'hosted_file';
|
||||
return `${$ae_api.base_url}/v3/action/${obj_type_path}/${file_id}/download?filename=${ae_util.clean_filename(final_filename)}&x_no_account_id_token=direct-download`;
|
||||
return `${$ae_api.base_url}/v3/action/${obj_type_path}/${file_id}/download?filename=${ae_util.clean_filename(final_filename)}&x_no_account_id_token=${$ae_api.account_id}`;
|
||||
});
|
||||
|
||||
async function handle_click() {
|
||||
const file_id = hosted_file_obj?.id || hosted_file_obj?.hosted_file_id || hosted_file_id;
|
||||
download_complete = undefined;
|
||||
download_status_msg = 'Downloading...';
|
||||
|
||||
if (click) {
|
||||
const result = click();
|
||||
// If the override returns a promise, track it so the UI shows progress
|
||||
if (result instanceof Promise) {
|
||||
ae_promises[file_id] = result;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ae_promises[file_id] = download_ae_obj_id__hosted_file({
|
||||
api_cfg: $ae_api,
|
||||
hosted_file_id: file_id,
|
||||
return_file: true,
|
||||
filename: final_filename,
|
||||
auto_download: auto_download,
|
||||
log_lvl: log_lvl
|
||||
})
|
||||
.then((result) => {
|
||||
if (result === null) {
|
||||
console.log('File not found (404)');
|
||||
download_complete = null;
|
||||
download_status_msg = 'File not found';
|
||||
} else if (result === false) {
|
||||
console.log(
|
||||
'Possible error with API server (check network and server status)'
|
||||
);
|
||||
download_complete = false;
|
||||
download_status_msg = 'Failed to download';
|
||||
} else {
|
||||
// console.log('File found and downloaded');
|
||||
download_complete = true;
|
||||
download_status_msg = 'File downloaded';
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet content()}
|
||||
@@ -246,36 +290,7 @@
|
||||
type="button"
|
||||
disabled={!$ae_loc.trusted_access}
|
||||
class={variant_classes}
|
||||
onclick={() => {
|
||||
download_complete = undefined;
|
||||
download_status_msg = 'Downloading...';
|
||||
ae_promises[file_id] = download_ae_obj_id__hosted_file({
|
||||
api_cfg: $ae_api,
|
||||
hosted_file_id: file_id,
|
||||
return_file: true,
|
||||
filename: final_filename,
|
||||
auto_download: auto_download,
|
||||
log_lvl: log_lvl
|
||||
})
|
||||
.then((result) => {
|
||||
if (result === null) {
|
||||
console.log('File not found (404)');
|
||||
download_complete = null;
|
||||
download_status_msg = 'File not found';
|
||||
} else if (result === false) {
|
||||
console.log(
|
||||
'Possible error with API server (check network and server status)'
|
||||
);
|
||||
download_complete = false;
|
||||
download_status_msg = 'Failed to download';
|
||||
} else {
|
||||
// console.log('File found and downloaded');
|
||||
download_complete = true;
|
||||
download_status_msg = 'File downloaded';
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}}
|
||||
onclick={handle_click}
|
||||
title={`Download this file:\n${final_filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...\nHosted ID: ${file_id}\n Linked to: ${linked_to_type} ID: ${linked_to_id}`}
|
||||
>
|
||||
{@render content()}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from 'svelte/legacy';
|
||||
|
||||
// Imports
|
||||
// Import components and elements
|
||||
import * as Lucide from 'lucide-svelte';
|
||||
import Element_input_files_tbl from '$lib/elements/element_input_files_tbl.svelte';
|
||||
|
||||
// Import storage, functions, and libraries
|
||||
@@ -76,8 +75,9 @@
|
||||
}
|
||||
|
||||
// *** Functions and Logic
|
||||
async function handle_submit_form_files(event: Event) {
|
||||
async function handle_submit_form_files(event: SubmitEvent) {
|
||||
console.log('*** handle_submit_form() ***');
|
||||
event.preventDefault();
|
||||
|
||||
if (!event) {
|
||||
return;
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
let hosted_file_results;
|
||||
|
||||
const target = event.target as HTMLFormElement;
|
||||
const target = event.currentTarget as HTMLFormElement;
|
||||
const file_input = target ? (target[input_element_id] as HTMLInputElement) : null;
|
||||
|
||||
if (
|
||||
@@ -264,10 +264,10 @@
|
||||
</script>
|
||||
|
||||
<!-- class:hidden={!$ae_loc.trusted_access} -->
|
||||
<form onsubmit={preventDefault(handle_submit_form_files)} class="{class_li_default} {class_li}">
|
||||
<form onsubmit={handle_submit_form_files} class="{class_li_default} {class_li}">
|
||||
{#await ae_promises.upload__hosted_file_obj}
|
||||
<div class="text-lg flex flex-row gap-1 items-center justify-center">
|
||||
<span class="fas fa-spinner fa-spin m-1"></span>
|
||||
<Lucide.Loader2 class="animate-spin m-1" />
|
||||
<span class="">
|
||||
Uploading
|
||||
{#if $ae_sess.api_upload_kv[task_id]}
|
||||
@@ -283,16 +283,13 @@
|
||||
class:hidden={$ae_sess.files.disable_submit__hosted_file_obj}
|
||||
>
|
||||
{#if label}{@render label()}{:else}
|
||||
<div>
|
||||
<span class="fas fa-upload"></span>
|
||||
<!-- Select files to upload -->
|
||||
<!-- <span class="fas fa-file-archive"></span> -->
|
||||
<strong class="bg-blue-300 p-1">Upload files</strong>
|
||||
<!-- (drag and drop) -->
|
||||
<div class="flex items-center justify-center gap-2 mb-2">
|
||||
<Lucide.Upload class="text-primary-500" />
|
||||
<strong class="preset-tonal-primary px-3 py-1 rounded-full">Select Files</strong>
|
||||
</div>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400 italic">
|
||||
<strong>Presentation related files only</strong><br />
|
||||
(PowerPoint, Keynote, PDF, mp4, Word Doc, Excel, txt, etc)
|
||||
<strong>Supported formats</strong><br />
|
||||
(PowerPoint, Keynote, PDF, Media, etc)
|
||||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
@@ -312,8 +309,8 @@
|
||||
block w-full text-lg
|
||||
text-gray-900
|
||||
border border-gray-300 rounded-lg
|
||||
cursor-pointer b
|
||||
g-gray-50 dark:text-gray-400 focus:outline-hidden dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400
|
||||
cursor-pointer
|
||||
bg-gray-50 dark:text-gray-400 focus:outline-hidden dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400
|
||||
{input_class_li.join(' ')}
|
||||
"
|
||||
class:hidden={$ae_sess.files.disable_submit__hosted_file_obj}
|
||||
@@ -328,32 +325,28 @@
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-lg btn-primary preset-tonal-primary border border-primary-500 hover:preset-tonal-success border border-success-500 w-54"
|
||||
class="btn btn-lg btn-primary preset-tonal-primary border border-primary-500 hover:preset-tonal-success hover:border-success-500 w-54"
|
||||
disabled={$ae_sess.files.disable_submit__hosted_file_obj ||
|
||||
$ae_sess.files.status__file_list != 'ready'}
|
||||
>
|
||||
{#await ae_promises.upload__hosted_file_obj}
|
||||
<span class="fas fa-spinner fa-spin m-1"></span>
|
||||
<Lucide.Loader2 class="animate-spin m-1" />
|
||||
<span class="">
|
||||
Uploading
|
||||
{#if $ae_sess.api_upload_kv[task_id]}
|
||||
{$ae_sess.api_upload_kv[task_id].percent_completed}%
|
||||
{:else}
|
||||
...
|
||||
{/if}
|
||||
</span>
|
||||
{:then}
|
||||
<span class="fas fa-upload m-1"></span>
|
||||
<span class="text-sm"> Upload? </span>
|
||||
<!-- <span class="fas fa-save m-1"></span> -->
|
||||
<span class="grow font-bold">
|
||||
<Lucide.UploadCloud class="m-1" size={20} />
|
||||
<span class="text-sm"> Upload </span>
|
||||
<span class="grow font-bold ml-2">
|
||||
{#if $ae_sess.files.processed_file_list?.length > 0}
|
||||
|
||||
{$ae_sess.files.processed_file_list.length == 1
|
||||
? `${$ae_sess.files.processed_file_list.length} file`
|
||||
: `${$ae_sess.files.processed_file_list.length} files`}
|
||||
{$ae_sess.files.processed_file_list.length} { $ae_sess.files.processed_file_list.length === 1 ? 'file' : 'files' }
|
||||
{:else}
|
||||
<span class="text-xs"> No files selected </span>
|
||||
<span class="text-xs"> 0 </span>
|
||||
{/if}
|
||||
<!-- Files -->
|
||||
</span>
|
||||
{/await}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user