updated protected files visualization
This commit is contained in:
parent
5eb54c7a3d
commit
1b8776926e
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
- Refreshed the OAuth2 Admin UI.
|
- Refreshed the OAuth2 Admin UI.
|
||||||
|
|
||||||
|
- Added auto "draft" to allow restoring previous record state in case of accidental reload or power outage.
|
||||||
|
|
||||||
|
|
||||||
## v0.14.5
|
## v0.14.5
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import ApiClient from "@/utils/ApiClient";
|
import ApiClient from "@/utils/ApiClient";
|
||||||
import CommonHelper from "@/utils/CommonHelper";
|
import CommonHelper from "@/utils/CommonHelper";
|
||||||
import PreviewPopup from "@/components/base/PreviewPopup.svelte";
|
import PreviewPopup from "@/components/base/PreviewPopup.svelte";
|
||||||
import { privateFilesCollectionsCache } from "@/stores/collections";
|
|
||||||
|
|
||||||
export let record = null;
|
export let record = null;
|
||||||
export let filename = "";
|
export let filename = "";
|
||||||
|
@ -14,16 +13,7 @@
|
||||||
let token = "";
|
let token = "";
|
||||||
let isLoadingToken = false;
|
let isLoadingToken = false;
|
||||||
|
|
||||||
$: withToken =
|
loadFileToken();
|
||||||
typeof $privateFilesCollectionsCache[record?.collectionId] !== "undefined"
|
|
||||||
? $privateFilesCollectionsCache[record?.collectionId]
|
|
||||||
: true;
|
|
||||||
|
|
||||||
$: if (withToken) {
|
|
||||||
loadFileToken();
|
|
||||||
} else {
|
|
||||||
token = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
$: type = CommonHelper.getFileType(filename);
|
$: type = CommonHelper.getFileType(filename);
|
||||||
|
|
||||||
|
@ -39,7 +29,7 @@
|
||||||
isLoadingToken = true;
|
isLoadingToken = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
token = await ApiClient.getAdminFileToken();
|
token = await ApiClient.getAdminFileToken(record.collectionId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("File token failure:", err);
|
console.warn("File token failure:", err);
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,6 +92,9 @@
|
||||||
initialDraft = getDraft();
|
initialDraft = getDraft();
|
||||||
if (!initialDraft || areRecordsEqual(record, initialDraft)) {
|
if (!initialDraft || areRecordsEqual(record, initialDraft)) {
|
||||||
initialDraft = null;
|
initialDraft = null;
|
||||||
|
} else {
|
||||||
|
delete initialDraft.password;
|
||||||
|
delete initialDraft.passwordConfirm;
|
||||||
}
|
}
|
||||||
|
|
||||||
originalSerializedData = JSON.stringify(record);
|
originalSerializedData = JSON.stringify(record);
|
||||||
|
@ -482,7 +485,7 @@
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{#if collection?.isAuth}
|
{#if collection?.isAuth}
|
||||||
<AuthFields bind:record {collection} />
|
<AuthFields bind:record {isNew} {collection} />
|
||||||
|
|
||||||
{#if collection?.schema?.length}
|
{#if collection?.schema?.length}
|
||||||
<hr />
|
<hr />
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
export let collection = new Collection();
|
export let collection = new Collection();
|
||||||
export let record = new Record();
|
export let record = new Record();
|
||||||
|
export let isNew = record.$isNew;
|
||||||
|
|
||||||
let originalUsername = record.username || null;
|
let originalUsername = record.username || null;
|
||||||
|
|
||||||
|
@ -28,15 +29,15 @@
|
||||||
|
|
||||||
<div class="grid m-b-base">
|
<div class="grid m-b-base">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<Field class="form-field {!record.$isNew ? 'required' : ''}" name="username" let:uniqueId>
|
<Field class="form-field {!isNew ? 'required' : ''}" name="username" let:uniqueId>
|
||||||
<label for={uniqueId}>
|
<label for={uniqueId}>
|
||||||
<i class={CommonHelper.getFieldTypeIcon("user")} />
|
<i class={CommonHelper.getFieldTypeIcon("user")} />
|
||||||
<span class="txt">Username</span>
|
<span class="txt">Username</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
requried={!record.$isNew}
|
requried={!isNew}
|
||||||
placeholder={record.$isNew ? "Leave empty to auto generate..." : originalUsername}
|
placeholder={isNew ? "Leave empty to auto generate..." : originalUsername}
|
||||||
id={uniqueId}
|
id={uniqueId}
|
||||||
bind:value={record.username}
|
bind:value={record.username}
|
||||||
/>
|
/>
|
||||||
|
@ -69,7 +70,7 @@
|
||||||
<!-- svelte-ignore a11y-autofocus -->
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
autofocus={record.$isNew}
|
autofocus={isNew}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id={uniqueId}
|
id={uniqueId}
|
||||||
required={collection.options?.requireEmail}
|
required={collection.options?.requireEmail}
|
||||||
|
@ -79,14 +80,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
{#if !record.$isNew}
|
{#if !isNew}
|
||||||
<Field class="form-field form-field-toggle" name="verified" let:uniqueId>
|
<Field class="form-field form-field-toggle" name="verified" let:uniqueId>
|
||||||
<input type="checkbox" id={uniqueId} bind:checked={changePasswordToggle} />
|
<input type="checkbox" id={uniqueId} bind:checked={changePasswordToggle} />
|
||||||
<label for={uniqueId}>Change password</label>
|
<label for={uniqueId}>Change password</label>
|
||||||
</Field>
|
</Field>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if record.$isNew || changePasswordToggle}
|
{#if isNew || changePasswordToggle}
|
||||||
<div class="block" transition:slide|local={{ duration: 150 }}>
|
<div class="block" transition:slide|local={{ duration: 150 }}>
|
||||||
<div class="grid" class:p-t-xs={changePasswordToggle}>
|
<div class="grid" class:p-t-xs={changePasswordToggle}>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
|
@ -131,7 +132,7 @@
|
||||||
id={uniqueId}
|
id={uniqueId}
|
||||||
bind:checked={record.verified}
|
bind:checked={record.verified}
|
||||||
on:change|preventDefault={(e) => {
|
on:change|preventDefault={(e) => {
|
||||||
if (record.$isNew) {
|
if (isNew) {
|
||||||
return; // no confirmation required
|
return; // no confirmation required
|
||||||
}
|
}
|
||||||
confirm(
|
confirm(
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import Field from "@/components/base/Field.svelte";
|
import Field from "@/components/base/Field.svelte";
|
||||||
import UploadedFilePreview from "@/components/base/UploadedFilePreview.svelte";
|
import UploadedFilePreview from "@/components/base/UploadedFilePreview.svelte";
|
||||||
import RecordFileThumb from "@/components/records/RecordFileThumb.svelte";
|
import RecordFileThumb from "@/components/records/RecordFileThumb.svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
export let record;
|
export let record;
|
||||||
export let value = "";
|
export let value = "";
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
let fileInput;
|
let fileInput;
|
||||||
let filesListElem;
|
let filesListElem;
|
||||||
let isDragOver = false;
|
let isDragOver = false;
|
||||||
|
let fileToken = "";
|
||||||
|
|
||||||
// normalize uploadedFiles type
|
// normalize uploadedFiles type
|
||||||
$: if (!Array.isArray(uploadedFiles)) {
|
$: if (!Array.isArray(uploadedFiles)) {
|
||||||
|
@ -93,6 +95,10 @@
|
||||||
|
|
||||||
uploadedFiles = uploadedFiles;
|
uploadedFiles = uploadedFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
fileToken = await ApiClient.getAdminFileToken(record.collectionId);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -129,7 +135,7 @@
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<a
|
<a
|
||||||
href={ApiClient.files.getUrl(record, filename)}
|
href={ApiClient.files.getUrl(record, filename, { token: fileToken })}
|
||||||
class="txt-ellipsis {isDeleted ? 'txt-strikethrough txt-hint' : 'link-primary'}"
|
class="txt-ellipsis {isDeleted ? 'txt-strikethrough txt-hint' : 'link-primary'}"
|
||||||
title="Download"
|
title="Download"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { writable } from "svelte/store";
|
||||||
import ApiClient from "@/utils/ApiClient";
|
import ApiClient from "@/utils/ApiClient";
|
||||||
import CommonHelper from "@/utils/CommonHelper";
|
import CommonHelper from "@/utils/CommonHelper";
|
||||||
|
|
||||||
export const collections = writable([]);
|
export const collections = writable([]);
|
||||||
export const activeCollection = writable({});
|
export const activeCollection = writable({});
|
||||||
export const isCollectionsLoading = writable(false);
|
export const isCollectionsLoading = writable(false);
|
||||||
export const privateFilesCollectionsCache = writable({});
|
export const protectedFilesCollectionsCache = writable({});
|
||||||
|
|
||||||
export function changeActiveCollectionById(collectionId) {
|
export function changeActiveCollectionById(collectionId) {
|
||||||
collections.update((list) => {
|
collections.update((list) => {
|
||||||
|
@ -82,7 +82,7 @@ export async function loadCollections(activeId = null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshProtectedFilesCollectionsCache() {
|
function refreshProtectedFilesCollectionsCache() {
|
||||||
privateFilesCollectionsCache.update((cache) => {
|
protectedFilesCollectionsCache.update((cache) => {
|
||||||
collections.update((current) => {
|
collections.update((current) => {
|
||||||
for (let c of current) {
|
for (let c of current) {
|
||||||
cache[c.id] = !!c.schema?.find((f) => f.type == "file" && f.options?.protected);
|
cache[c.id] = !!c.schema?.find((f) => f.type == "file" && f.options?.protected);
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import PocketBase, { LocalAuthStore, Admin, isTokenExpired } from "pocketbase";
|
import PocketBase, { LocalAuthStore, Admin, isTokenExpired } from "pocketbase";
|
||||||
// ---
|
// ---
|
||||||
import CommonHelper from "@/utils/CommonHelper";
|
import CommonHelper from "@/utils/CommonHelper";
|
||||||
import { replace } from "svelte-spa-router";
|
import { replace } from "svelte-spa-router";
|
||||||
import { addErrorToast } from "@/stores/toasts";
|
import { get } from "svelte/store";
|
||||||
import { setErrors } from "@/stores/errors";
|
import { addErrorToast } from "@/stores/toasts";
|
||||||
import { setAdmin } from "@/stores/admin";
|
import { setErrors } from "@/stores/errors";
|
||||||
|
import { setAdmin } from "@/stores/admin";
|
||||||
|
import { protectedFilesCollectionsCache } from "@/stores/collections";
|
||||||
|
|
||||||
|
|
||||||
const adminFileTokenKey = "pb_admin_file_token";
|
const adminFileTokenKey = "pb_admin_file_token";
|
||||||
|
|
||||||
|
@ -17,7 +20,7 @@ PocketBase.prototype.logout = function(redirect = true) {
|
||||||
this.authStore.clear();
|
this.authStore.clear();
|
||||||
|
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
replace('/login');
|
replace("/login");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,7 +31,7 @@ PocketBase.prototype.logout = function(redirect = true) {
|
||||||
* @param {Boolean} notify Whether to add a toast notification.
|
* @param {Boolean} notify Whether to add a toast notification.
|
||||||
* @param {String} defaultMsg Default toast notification message if the error doesn't have one.
|
* @param {String} defaultMsg Default toast notification message if the error doesn't have one.
|
||||||
*/
|
*/
|
||||||
PocketBase.prototype.errorResponseHandler = function(err, notify = true, defaultMsg = '') {
|
PocketBase.prototype.errorResponseHandler = function(err, notify = true, defaultMsg = "") {
|
||||||
if (!err || !(err instanceof Error) || err.isAbort) {
|
if (!err || !(err instanceof Error) || err.isAbort) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -61,15 +64,28 @@ PocketBase.prototype.errorResponseHandler = function(err, notify = true, default
|
||||||
// forbidden
|
// forbidden
|
||||||
if (statusCode === 403) {
|
if (statusCode === 403) {
|
||||||
this.cancelAllRequests();
|
this.cancelAllRequests();
|
||||||
return replace('/');
|
return replace("/");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {Promise<String>}
|
* @return {Promise<String>}
|
||||||
*/
|
*/
|
||||||
PocketBase.prototype.getAdminFileToken = async function() {
|
PocketBase.prototype.getAdminFileToken = async function(collectionId = "") {
|
||||||
let token = localStorage.getItem(adminFileTokenKey) || '';
|
let needToken = true;
|
||||||
|
|
||||||
|
if (collectionId) {
|
||||||
|
const protectedCollections = get(protectedFilesCollectionsCache);
|
||||||
|
needToken = typeof protectedCollections[collectionId] !== "undefined"
|
||||||
|
? protectedCollections[collectionId]
|
||||||
|
: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needToken) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = localStorage.getItem(adminFileTokenKey) || "";
|
||||||
|
|
||||||
// request a new token only if the previous one is missing or will expire soon
|
// request a new token only if the previous one is missing or will expire soon
|
||||||
if (!token || isTokenExpired(token, 15)) {
|
if (!token || isTokenExpired(token, 15)) {
|
||||||
|
|
Loading…
Reference in New Issue