@brftech/filex-core 0.30.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/filex-core.js +9718 -8611
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +95 -87
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +368 -64
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +599 -19
- package/src/components/Breadcrumb.vue +4 -2
- package/src/components/E2eRecoveryUnlockModal.vue +168 -0
- package/src/components/EncryptedFolderModal.vue +10 -0
- package/src/components/GalleryView.vue +37 -0
- package/src/components/GridView.vue +39 -0
- package/src/components/ListView.vue +1 -0
- package/src/components/RecoveryKeyModal.vue +133 -0
- package/src/components/SideNav.vue +168 -5
- package/src/components/StarButton.vue +27 -15
- package/src/composables/useFileApi.ts +38 -0
- package/src/composables/useKeyboardShortcuts.ts +7 -0
- package/src/index.ts +27 -1
- package/src/lib/e2ecrypto.ts +528 -70
- package/src/lib/listing.ts +56 -1
- package/src/lib/star.ts +42 -0
- package/src/lib/tags.ts +105 -0
- package/src/locales/en.ts +71 -2
- package/src/locales/tr.ts +71 -2
- package/src/styles/base.css +252 -0
- package/src/types/ExplorerConfig.ts +36 -0
- package/src/types/FileNode.ts +23 -0
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
|
23
23
|
import { hasInternalDrag } from '../lib/dragOut';
|
|
24
|
-
import {
|
|
24
|
+
import { virtualSegmentLabel } from '../lib/listing';
|
|
25
25
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
26
26
|
import { useLocale } from '../composables/useLocale';
|
|
27
27
|
|
|
@@ -125,7 +125,9 @@ const crumbs = computed<Crumb[]>(() => {
|
|
|
125
125
|
// gezinti:g1 — the sentinel segments the virtual views park in `dirname`
|
|
126
126
|
// (`.trash` predates them). Without a mapping the crumb reads ".starred",
|
|
127
127
|
// which is a filename the user never typed and cannot navigate to.
|
|
128
|
-
|
|
128
|
+
// etiket:t1 — via the shared resolver, which also knows the tag view's
|
|
129
|
+
// `.tag~<name>` segment; the raw map only covers the fixed-label views.
|
|
130
|
+
const label = virtualSegmentLabel(part, t) || part;
|
|
129
131
|
out.push({ label, adapterPath: `${adapterPrefix}${acc}` });
|
|
130
132
|
}
|
|
131
133
|
return out;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* E2eRecoveryUnlockModal — open an encrypted folder without its password.
|
|
4
|
+
*
|
|
5
|
+
* Two doors, and the dialog is explicit about which one you are using
|
|
6
|
+
* because they are not equivalent:
|
|
7
|
+
*
|
|
8
|
+
* - the USER RECOVERY KEY, shown once when the folder was created. Opens
|
|
9
|
+
* the folder and nobody is told, because it is your key.
|
|
10
|
+
* - the ESCROW KEY, held by the operator of this installation. Opening a
|
|
11
|
+
* folder with it NOTIFIES the folder's owner. The dialog says so before
|
|
12
|
+
* the key is typed, not after.
|
|
13
|
+
*
|
|
14
|
+
* The component collects input and validates its shape. All crypto and all
|
|
15
|
+
* network calls happen in the parent (FileExplorer), which owns the marker
|
|
16
|
+
* and the key ring — same division as the password lock screen.
|
|
17
|
+
*/
|
|
18
|
+
import { computed, ref, watch } from 'vue';
|
|
19
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
20
|
+
import { useLocale } from '../composables/useLocale';
|
|
21
|
+
import { parseRecoveryKey } from '../lib/e2ecrypto';
|
|
22
|
+
import Modal from '../modals/Modal.vue';
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{
|
|
25
|
+
open: boolean;
|
|
26
|
+
locale: LocaleCode;
|
|
27
|
+
/** The folder has a user recovery key slot (v2 markers created since 0.31). */
|
|
28
|
+
hasRecovery: boolean;
|
|
29
|
+
/** The folder has an escrow slot AND this installation has escrow enabled. */
|
|
30
|
+
hasEscrow: boolean;
|
|
31
|
+
/** Short id of the escrow key the folder was sealed to. */
|
|
32
|
+
escrowKid?: string | null;
|
|
33
|
+
busy?: boolean;
|
|
34
|
+
/** Set by the parent after a failed attempt. */
|
|
35
|
+
error?: string | null;
|
|
36
|
+
}>();
|
|
37
|
+
|
|
38
|
+
const emit = defineEmits<{
|
|
39
|
+
(e: 'close'): void;
|
|
40
|
+
(e: 'submit', payload: { mode: 'recovery' | 'escrow'; value: string }): void;
|
|
41
|
+
}>();
|
|
42
|
+
|
|
43
|
+
const { t } = useLocale(() => props.locale);
|
|
44
|
+
const mode = ref<'recovery' | 'escrow'>('recovery');
|
|
45
|
+
const recoveryValue = ref('');
|
|
46
|
+
const escrowValue = ref('');
|
|
47
|
+
const localErr = ref<string | null>(null);
|
|
48
|
+
|
|
49
|
+
watch(
|
|
50
|
+
() => props.open,
|
|
51
|
+
(v) => {
|
|
52
|
+
if (v) {
|
|
53
|
+
mode.value = props.hasRecovery ? 'recovery' : 'escrow';
|
|
54
|
+
recoveryValue.value = '';
|
|
55
|
+
escrowValue.value = '';
|
|
56
|
+
localErr.value = null;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/** A folder with neither slot is a pre-0.31 folder: the password is the
|
|
62
|
+
* only way in, and saying so is more useful than an empty dialog. */
|
|
63
|
+
const nothingAvailable = computed(() => !props.hasRecovery && !props.hasEscrow);
|
|
64
|
+
|
|
65
|
+
const shownError = computed(() => localErr.value || props.error || null);
|
|
66
|
+
|
|
67
|
+
function submit() {
|
|
68
|
+
if (props.busy || nothingAvailable.value) return;
|
|
69
|
+
localErr.value = null;
|
|
70
|
+
if (mode.value === 'recovery') {
|
|
71
|
+
// Check the shape locally so a typo reads as a typo rather than as a
|
|
72
|
+
// wrong key — the two failures call for different next steps.
|
|
73
|
+
if (!parseRecoveryKey(recoveryValue.value)) {
|
|
74
|
+
localErr.value = t('e2e.recover.bad_format');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
emit('submit', { mode: 'recovery', value: recoveryValue.value });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!escrowValue.value.trim()) {
|
|
81
|
+
localErr.value = t('e2e.recover.escrow_required');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
emit('submit', { mode: 'escrow', value: escrowValue.value });
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<Modal :open="open" :title="t('e2e.recover.title')" size="sm" @close="emit('close')">
|
|
90
|
+
<div class="fe-e2e-recover">
|
|
91
|
+
<p v-if="nothingAvailable" class="fe-e2e-recover__none">
|
|
92
|
+
{{ t('e2e.recover.none') }}
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
<template v-else>
|
|
96
|
+
<div v-if="hasRecovery && hasEscrow" class="fe-e2e-recover__tabs" role="tablist">
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
role="tab"
|
|
100
|
+
class="fe-e2e-recover__tab"
|
|
101
|
+
:class="{ 'fe-e2e-recover__tab--on': mode === 'recovery' }"
|
|
102
|
+
:aria-selected="mode === 'recovery'"
|
|
103
|
+
@click="mode = 'recovery'"
|
|
104
|
+
>
|
|
105
|
+
{{ t('e2e.recover.tab_recovery') }}
|
|
106
|
+
</button>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
role="tab"
|
|
110
|
+
class="fe-e2e-recover__tab"
|
|
111
|
+
:class="{ 'fe-e2e-recover__tab--on': mode === 'escrow' }"
|
|
112
|
+
:aria-selected="mode === 'escrow'"
|
|
113
|
+
@click="mode = 'escrow'"
|
|
114
|
+
>
|
|
115
|
+
{{ t('e2e.recover.tab_escrow') }}
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<form v-if="mode === 'recovery'" class="fe-e2e-form" @submit.prevent="submit">
|
|
120
|
+
<p class="fe-e2e-recover__hint">{{ t('e2e.recover.recovery_hint') }}</p>
|
|
121
|
+
<input
|
|
122
|
+
v-model="recoveryValue"
|
|
123
|
+
type="text"
|
|
124
|
+
class="fe-input fe-e2e-recover__key"
|
|
125
|
+
:placeholder="t('e2e.recover.recovery_placeholder')"
|
|
126
|
+
autocomplete="off"
|
|
127
|
+
spellcheck="false"
|
|
128
|
+
:disabled="busy"
|
|
129
|
+
/>
|
|
130
|
+
</form>
|
|
131
|
+
|
|
132
|
+
<form v-else class="fe-e2e-form" @submit.prevent="submit">
|
|
133
|
+
<div class="fe-e2e-warn" role="alert">
|
|
134
|
+
<strong>{{ t('e2e.recover.escrow_warn_title') }}</strong>
|
|
135
|
+
<p>{{ t('e2e.recover.escrow_warn_body') }}</p>
|
|
136
|
+
</div>
|
|
137
|
+
<p v-if="escrowKid" class="fe-e2e-recover__hint">
|
|
138
|
+
{{ t('e2e.recover.escrow_kid') }}: <code>{{ escrowKid }}</code>
|
|
139
|
+
</p>
|
|
140
|
+
<textarea
|
|
141
|
+
v-model="escrowValue"
|
|
142
|
+
class="fe-input fe-e2e-recover__escrow"
|
|
143
|
+
rows="5"
|
|
144
|
+
:placeholder="t('e2e.recover.escrow_placeholder')"
|
|
145
|
+
autocomplete="off"
|
|
146
|
+
spellcheck="false"
|
|
147
|
+
:disabled="busy"
|
|
148
|
+
></textarea>
|
|
149
|
+
</form>
|
|
150
|
+
</template>
|
|
151
|
+
|
|
152
|
+
<p v-if="shownError" class="fe-form__error">{{ shownError }}</p>
|
|
153
|
+
</div>
|
|
154
|
+
<template #actions>
|
|
155
|
+
<button type="button" class="fe-btn" :disabled="busy" @click="emit('close')">
|
|
156
|
+
{{ t('modal.newfolder.cancel') }}
|
|
157
|
+
</button>
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
class="fe-btn fe-btn--primary"
|
|
161
|
+
:disabled="busy || nothingAvailable"
|
|
162
|
+
@click="submit"
|
|
163
|
+
>
|
|
164
|
+
{{ busy ? t('e2e.recover.busy') : t('e2e.recover.unlock') }}
|
|
165
|
+
</button>
|
|
166
|
+
</template>
|
|
167
|
+
</Modal>
|
|
168
|
+
</template>
|
|
@@ -20,6 +20,10 @@ const props = defineProps<{
|
|
|
20
20
|
locale: LocaleCode;
|
|
21
21
|
/** True while the parent is creating the folder + uploading the marker. */
|
|
22
22
|
busy?: boolean;
|
|
23
|
+
/** Short id of this installation's E2E escrow key, when one is configured.
|
|
24
|
+
* Shown BEFORE the folder is created: escrow means the operator can open
|
|
25
|
+
* it without the password, and that is not a detail to discover later. */
|
|
26
|
+
escrowKid?: string | null;
|
|
23
27
|
}>();
|
|
24
28
|
|
|
25
29
|
const emit = defineEmits<{
|
|
@@ -107,6 +111,12 @@ function submit() {
|
|
|
107
111
|
<strong>{{ t('e2e.create.warn_title') }}</strong>
|
|
108
112
|
<p>{{ t('e2e.create.warn_body') }}</p>
|
|
109
113
|
</div>
|
|
114
|
+
<!-- wiring:e2 recovery — disclosure, not a setting. The user cannot
|
|
115
|
+
turn escrow off for their folder; they can only know about it. -->
|
|
116
|
+
<div v-if="escrowKid" class="fe-e2e-rk__escrow" role="note">
|
|
117
|
+
<strong>{{ t('e2e.create.escrow_title') }}</strong>
|
|
118
|
+
<p>{{ t('e2e.create.escrow_body') }}</p>
|
|
119
|
+
</div>
|
|
110
120
|
<label class="fe-e2e-ack">
|
|
111
121
|
<input v-model="ack" type="checkbox" :disabled="busy" />
|
|
112
122
|
<span>{{ t('e2e.create.ack') }}</span>
|
|
@@ -14,6 +14,7 @@ import type { FileNode } from '../types/FileNode';
|
|
|
14
14
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
15
15
|
import { useLocale } from '../composables/useLocale';
|
|
16
16
|
import { fileIconSvg } from '../lib/fileIcons';
|
|
17
|
+
import StarButton from './StarButton.vue';
|
|
17
18
|
import { applyDragGhost } from '../lib/dragGhost';
|
|
18
19
|
|
|
19
20
|
const props = defineProps<{
|
|
@@ -27,6 +28,19 @@ const props = defineProps<{
|
|
|
27
28
|
* GridView: raw `thumb_url` is root-relative and unauthenticated, so
|
|
28
29
|
* embedded hosts NEED this. null = icon fallback. */
|
|
29
30
|
thumbSrc?: (n: FileNode) => string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Starring on a card. Same contract as ListView: the id set the explorer
|
|
33
|
+
* keeps, plus the API wiring StarButton needs. Absent apiBase → no star is
|
|
34
|
+
* rendered at all, exactly as in the list.
|
|
35
|
+
*
|
|
36
|
+
* ⚠ Not list-only. The Starred VIEW shipped before starring was reachable
|
|
37
|
+
* from anywhere but the list, so a user in grid view — the mode the panel's
|
|
38
|
+
* own screenshots show — had a view they could not fill.
|
|
39
|
+
*/
|
|
40
|
+
starredIds?: Set<number>;
|
|
41
|
+
apiBase?: string;
|
|
42
|
+
authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
43
|
+
authCredentials?: RequestCredentials;
|
|
30
44
|
}>();
|
|
31
45
|
|
|
32
46
|
const emit = defineEmits<{
|
|
@@ -35,6 +49,7 @@ const emit = defineEmits<{
|
|
|
35
49
|
(e: 'context-card', node: FileNode, ev: MouseEvent): void;
|
|
36
50
|
(e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
|
|
37
51
|
(e: 'item-drop-into', target: FileNode, ev: DragEvent): void;
|
|
52
|
+
(e: 'star-change', node: FileNode, value: boolean): void;
|
|
38
53
|
}>();
|
|
39
54
|
|
|
40
55
|
const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
|
|
@@ -43,6 +58,12 @@ function thumbOf(n: FileNode): string | null {
|
|
|
43
58
|
return props.thumbSrc ? props.thumbSrc(n) : (n.thumb_url ?? null);
|
|
44
59
|
}
|
|
45
60
|
|
|
61
|
+
/** A card carries a star when the host wired the API and the node is a file
|
|
62
|
+
* with a server id — the same rule the list row uses. */
|
|
63
|
+
function canStar(n: FileNode): boolean {
|
|
64
|
+
return props.apiBase !== undefined && typeof n.id === 'number' && n.type === 'file';
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
function isSelected(n: FileNode): boolean {
|
|
47
68
|
return props.selected.has(n.path);
|
|
48
69
|
}
|
|
@@ -196,6 +217,22 @@ function metaFor(n: FileNode): string {
|
|
|
196
217
|
<span v-else-if="specialEmojiFor(n)" class="fe-gal__icon">{{ specialEmojiFor(n) }}</span>
|
|
197
218
|
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
198
219
|
<span v-else class="fe-gal__icon fe-gal__icon--svg" v-html="fileIconSvg(n)"></span>
|
|
220
|
+
<!-- Same star chip as the grid, same component, same rule: painted
|
|
221
|
+
when starred, on hover/focus otherwise. It sits above .fe-gal__meta
|
|
222
|
+
(which is aria-hidden and covers the tile's foot on hover). -->
|
|
223
|
+
<div v-if="canStar(n)" class="fe-gal__star" @click.stop @dblclick.stop>
|
|
224
|
+
<StarButton
|
|
225
|
+
:starred="!!starredIds?.has(n.id!)"
|
|
226
|
+
:node-id="n.id!"
|
|
227
|
+
:api-base="apiBase"
|
|
228
|
+
:auth-headers="authHeaders"
|
|
229
|
+
:auth-credentials="authCredentials"
|
|
230
|
+
:locale="locale"
|
|
231
|
+
compact
|
|
232
|
+
card
|
|
233
|
+
@change="(val: boolean) => emit('star-change', n, val)"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
199
236
|
<div class="fe-gal__meta" aria-hidden="true">
|
|
200
237
|
<span v-if="metaFor(n)" class="fe-gal__meta-line">{{ metaFor(n) }}</span>
|
|
201
238
|
<span
|
|
@@ -8,6 +8,7 @@ import { hasInternalDrag } from '../lib/dragOut';
|
|
|
8
8
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
9
9
|
import { useLocale } from '../composables/useLocale';
|
|
10
10
|
import { fileIconSvg } from '../lib/fileIcons';
|
|
11
|
+
import StarButton from './StarButton.vue';
|
|
11
12
|
import { snippetSegments } from '../lib/snippet'; /* bul:s3 */
|
|
12
13
|
import { applyDragGhost } from '../lib/dragGhost'; /* wiring:c4 */
|
|
13
14
|
|
|
@@ -25,6 +26,19 @@ const props = defineProps<{
|
|
|
25
26
|
/** Desktop selective sync: availability badge per tile. Absent on the
|
|
26
27
|
* web — no badge renders at all. */
|
|
27
28
|
keepBadgeFor?: (n: FileNode) => 'kept' | 'syncing' | 'cloud' | 'partial' | null;
|
|
29
|
+
/**
|
|
30
|
+
* Starring on a card. Same contract as ListView: the id set the explorer
|
|
31
|
+
* keeps, plus the API wiring StarButton needs. Absent apiBase → no star is
|
|
32
|
+
* rendered at all, exactly as in the list.
|
|
33
|
+
*
|
|
34
|
+
* ⚠ Not list-only. The Starred VIEW shipped before starring was reachable
|
|
35
|
+
* from anywhere but the list, so a user in grid view — the mode the panel's
|
|
36
|
+
* own screenshots show — had a view they could not fill.
|
|
37
|
+
*/
|
|
38
|
+
starredIds?: Set<number>;
|
|
39
|
+
apiBase?: string;
|
|
40
|
+
authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
41
|
+
authCredentials?: RequestCredentials;
|
|
28
42
|
}>();
|
|
29
43
|
|
|
30
44
|
const emit = defineEmits<{
|
|
@@ -33,6 +47,7 @@ const emit = defineEmits<{
|
|
|
33
47
|
(e: 'context-card', node: FileNode, ev: MouseEvent): void;
|
|
34
48
|
(e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
|
|
35
49
|
(e: 'item-drop-into', target: FileNode, ev: DragEvent): void;
|
|
50
|
+
(e: 'star-change', node: FileNode, value: boolean): void;
|
|
36
51
|
}>();
|
|
37
52
|
|
|
38
53
|
const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
|
|
@@ -43,6 +58,12 @@ function thumbOf(n: FileNode): string | null {
|
|
|
43
58
|
return props.thumbSrc ? props.thumbSrc(n) : (n.thumb_url ?? null);
|
|
44
59
|
}
|
|
45
60
|
|
|
61
|
+
/** A card carries a star when the host wired the API and the node is a file
|
|
62
|
+
* with a server id — the same rule the list row uses. */
|
|
63
|
+
function canStar(n: FileNode): boolean {
|
|
64
|
+
return props.apiBase !== undefined && typeof n.id === 'number' && n.type === 'file';
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
function isSelected(n: FileNode): boolean {
|
|
47
68
|
return props.selected.has(n.path);
|
|
48
69
|
}
|
|
@@ -213,6 +234,24 @@ function snippetTitle(snippet: string): string {
|
|
|
213
234
|
<span v-else-if="specialEmojiFor(n)" class="fe-grid__icon">{{ specialEmojiFor(n) }}</span>
|
|
214
235
|
<!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
|
|
215
236
|
<span v-else class="fe-grid__icon fe-grid__icon--svg" v-html="fileIconSvg(n)"></span>
|
|
237
|
+
<!-- Star, ON the tile. A hover-only affordance would be invisible to
|
|
238
|
+
the person looking for what they starred, so the chip is always
|
|
239
|
+
painted once the file IS starred and only appears on hover/focus
|
|
240
|
+
otherwise (see .fe-grid__star in styles/base.css). @click.stop so
|
|
241
|
+
starring never doubles as a card selection. -->
|
|
242
|
+
<div v-if="canStar(n)" class="fe-grid__star" @click.stop @dblclick.stop>
|
|
243
|
+
<StarButton
|
|
244
|
+
:starred="!!starredIds?.has(n.id!)"
|
|
245
|
+
:node-id="n.id!"
|
|
246
|
+
:api-base="apiBase"
|
|
247
|
+
:auth-headers="authHeaders"
|
|
248
|
+
:auth-credentials="authCredentials"
|
|
249
|
+
:locale="locale"
|
|
250
|
+
compact
|
|
251
|
+
card
|
|
252
|
+
@change="(val: boolean) => emit('star-change', n, val)"
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
216
255
|
</div>
|
|
217
256
|
<div class="fe-grid__label" :title="n.basename">
|
|
218
257
|
{{ nodeDisplayName(n) }}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* RecoveryKeyModal — shows a folder recovery key, exactly once (wiring:e2).
|
|
4
|
+
*
|
|
5
|
+
* This is the only moment the key exists anywhere outside the user's own
|
|
6
|
+
* records. filex does not store it, cannot recompute it, and will never be
|
|
7
|
+
* able to show it again — so the dialog is deliberately hard to dismiss by
|
|
8
|
+
* accident: no backdrop close, no cancel button, and an acknowledgement the
|
|
9
|
+
* user has to tick.
|
|
10
|
+
*
|
|
11
|
+
* ⚠ The key must not leave this component. No logging, no analytics, no
|
|
12
|
+
* emit carrying its value. Copy and download are both local.
|
|
13
|
+
*
|
|
14
|
+
* Crypto scheme: docs/E2E-ENCRYPTION.md and lib/e2ecrypto.ts.
|
|
15
|
+
*/
|
|
16
|
+
import { computed, ref, watch } from 'vue';
|
|
17
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
18
|
+
import { useLocale } from '../composables/useLocale';
|
|
19
|
+
import Modal from '../modals/Modal.vue';
|
|
20
|
+
|
|
21
|
+
const props = defineProps<{
|
|
22
|
+
open: boolean;
|
|
23
|
+
locale: LocaleCode;
|
|
24
|
+
/** The key itself. Shown, copied, downloaded — never emitted or stored. */
|
|
25
|
+
recoveryKey: string;
|
|
26
|
+
/** Folder name, for the downloaded file and the dialog copy. */
|
|
27
|
+
folderName?: string;
|
|
28
|
+
/** Set when this installation holds an escrow key for the folder too. */
|
|
29
|
+
escrowKid?: string | null;
|
|
30
|
+
/** 'created' = a new folder; 'upgraded' = an existing folder gained recovery. */
|
|
31
|
+
variant?: 'created' | 'upgraded';
|
|
32
|
+
}>();
|
|
33
|
+
|
|
34
|
+
const emit = defineEmits<{ (e: 'close'): void }>();
|
|
35
|
+
|
|
36
|
+
const { t } = useLocale(() => props.locale);
|
|
37
|
+
const ack = ref(false);
|
|
38
|
+
const copied = ref(false);
|
|
39
|
+
|
|
40
|
+
watch(
|
|
41
|
+
() => props.open,
|
|
42
|
+
(v) => {
|
|
43
|
+
if (v) {
|
|
44
|
+
ack.value = false;
|
|
45
|
+
copied.value = false;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const title = computed(() =>
|
|
51
|
+
props.variant === 'upgraded' ? t('e2e.recovery.title_upgraded') : t('e2e.recovery.title'),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
async function copy() {
|
|
55
|
+
try {
|
|
56
|
+
await navigator.clipboard.writeText(props.recoveryKey);
|
|
57
|
+
copied.value = true;
|
|
58
|
+
window.setTimeout(() => (copied.value = false), 2000);
|
|
59
|
+
} catch {
|
|
60
|
+
// Clipboard permission denied (or a non-secure origin). The key is on
|
|
61
|
+
// screen and selectable, so this is a convenience, not the only route.
|
|
62
|
+
copied.value = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function download() {
|
|
67
|
+
const name = props.folderName || 'folder';
|
|
68
|
+
const body =
|
|
69
|
+
`filex recovery key\n` +
|
|
70
|
+
`folder: ${name}\n` +
|
|
71
|
+
`\n${props.recoveryKey}\n\n` +
|
|
72
|
+
`This key opens the encrypted folder without its password.\n` +
|
|
73
|
+
`Anyone holding it can read the folder. Store it like a password.\n` +
|
|
74
|
+
`filex does not keep a copy and cannot show it again.\n`;
|
|
75
|
+
const url = URL.createObjectURL(new Blob([body], { type: 'text/plain' }));
|
|
76
|
+
const a = document.createElement('a');
|
|
77
|
+
a.href = url;
|
|
78
|
+
a.download = `filex-recovery-${name}.txt`;
|
|
79
|
+
document.body.appendChild(a);
|
|
80
|
+
a.click();
|
|
81
|
+
a.remove();
|
|
82
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 30_000);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function done() {
|
|
86
|
+
if (!ack.value) return;
|
|
87
|
+
emit('close');
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<!-- No backdrop close, and ESC only lands once the box is ticked: the key
|
|
93
|
+
cannot be shown again, so an accidental dismissal is data loss. -->
|
|
94
|
+
<Modal :open="open" :title="title" size="sm" :close-on-backdrop="false" @close="done">
|
|
95
|
+
<div class="fe-e2e-rk">
|
|
96
|
+
<p class="fe-e2e-rk__lead">
|
|
97
|
+
{{ variant === 'upgraded' ? t('e2e.recovery.lead_upgraded') : t('e2e.recovery.lead') }}
|
|
98
|
+
</p>
|
|
99
|
+
|
|
100
|
+
<output class="fe-e2e-rk__key" aria-label="recovery key">{{ recoveryKey }}</output>
|
|
101
|
+
|
|
102
|
+
<div class="fe-e2e-rk__actions">
|
|
103
|
+
<button type="button" class="fe-btn" @click="copy">
|
|
104
|
+
{{ copied ? t('e2e.recovery.copied') : t('e2e.recovery.copy') }}
|
|
105
|
+
</button>
|
|
106
|
+
<button type="button" class="fe-btn" @click="download">
|
|
107
|
+
{{ t('e2e.recovery.download') }}
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="fe-e2e-warn" role="alert">
|
|
112
|
+
<strong>{{ t('e2e.recovery.warn_title') }}</strong>
|
|
113
|
+
<p>{{ t('e2e.recovery.warn_body') }}</p>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div v-if="escrowKid" class="fe-e2e-rk__escrow" role="note">
|
|
117
|
+
<strong>{{ t('e2e.recovery.escrow_title') }}</strong>
|
|
118
|
+
<p>{{ t('e2e.recovery.escrow_body') }}</p>
|
|
119
|
+
<p class="fe-e2e-rk__kid">{{ t('e2e.recovery.escrow_kid') }}: <code>{{ escrowKid }}</code></p>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<label class="fe-e2e-ack">
|
|
123
|
+
<input v-model="ack" type="checkbox" />
|
|
124
|
+
<span>{{ t('e2e.recovery.ack') }}</span>
|
|
125
|
+
</label>
|
|
126
|
+
</div>
|
|
127
|
+
<template #actions>
|
|
128
|
+
<button type="button" class="fe-btn fe-btn--primary" :disabled="!ack" @click="done">
|
|
129
|
+
{{ t('e2e.recovery.done') }}
|
|
130
|
+
</button>
|
|
131
|
+
</template>
|
|
132
|
+
</Modal>
|
|
133
|
+
</template>
|