@brftech/filex-core 0.19.0 → 0.20.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.
Files changed (39) hide show
  1. package/README.md +34 -4
  2. package/dist/filex-core.js +10626 -6501
  3. package/dist/filex-core.js.map +1 -1
  4. package/dist/filex-core.umd.cjs +94 -63
  5. package/dist/filex-core.umd.cjs.map +1 -1
  6. package/dist/index.d.ts +1026 -7
  7. package/dist/style.css +1 -1
  8. package/package.json +3 -3
  9. package/src/FileExplorer.vue +103 -68
  10. package/src/components/ConnectionGuideView.vue +333 -0
  11. package/src/components/ConnectionsPanel.vue +912 -0
  12. package/src/components/NFSExportsPanel.vue +283 -0
  13. package/src/components/S3KeysPanel.vue +381 -0
  14. package/src/components/SSHKeysPanel.vue +222 -0
  15. package/src/components/StorageFields.vue +362 -0
  16. package/src/components/TokensPanel.vue +191 -0
  17. package/src/components/UploadProgress.vue +5 -1
  18. package/src/composables/useConnections.ts +271 -0
  19. package/src/composables/useFileApi.ts +15 -2
  20. package/src/composables/useNFSExports.ts +148 -0
  21. package/src/composables/useS3Keys.ts +175 -0
  22. package/src/composables/useSSHKeys.ts +119 -0
  23. package/src/composables/useThumbs.ts +1 -1
  24. package/src/composables/useTokens.ts +121 -0
  25. package/src/composables/useUploadChunked.ts +433 -164
  26. package/src/index.ts +77 -2
  27. package/src/lib/connectionGuides.ts +1279 -0
  28. package/src/lib/realtime.ts +1 -1
  29. package/src/lib/uploadResume.ts +157 -0
  30. package/src/locales/en.ts +413 -0
  31. package/src/locales/tr.ts +416 -0
  32. package/src/modals/ConvertModal.vue +1 -1
  33. package/src/styles/base.css +12 -12
  34. package/src/types/Connections.ts +122 -0
  35. package/src/types/ExplorerConfig.ts +23 -2
  36. package/src/types/NFSExports.ts +47 -0
  37. package/src/types/S3Keys.ts +55 -0
  38. package/src/types/SSHKeys.ts +54 -0
  39. package/src/types/Tokens.ts +39 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * useS3Keys — the one client for the S3 access-key surface.
3
+ *
4
+ * It goes through `useFileApi`, the same auth plumbing the explorer uses, so
5
+ * the desktop app's function-token, the web app's cookie and an embedder's
6
+ * bearer all work here without this file knowing which is which.
7
+ *
8
+ * ⚠ Every surface mounts THIS, not a copy. The keys panel, the guide and the
9
+ * copy buttons exist once in `packages/core`; the web app and the desktop app
10
+ * render the same component. That is the standing rule ("never write
11
+ * surface-specific behaviour") applied to a credential surface, where a
12
+ * divergence would mean one surface handing out keys the other cannot revoke.
13
+ */
14
+
15
+ import { computed, ref, shallowRef } from 'vue';
16
+ import type { ExplorerConfig } from '../types/ExplorerConfig';
17
+ import type { S3AccessKey, S3Connection, S3KeyCreated, S3KeyRequest } from '../types/S3Keys';
18
+ import { connectionsBase } from './useConnections';
19
+ import { useFileApi } from './useFileApi';
20
+
21
+ export function useS3Keys(config: ExplorerConfig) {
22
+ const api = useFileApi(config);
23
+ const base = connectionsBase(config);
24
+ const url = (path: string) => `${base}${path}`;
25
+
26
+ const keys = shallowRef<S3AccessKey[]>([]);
27
+ const connection = ref<S3Connection | null>(null);
28
+ const loading = ref(false);
29
+ const loaded = ref(false);
30
+ const error = ref<string | null>(null);
31
+ /**
32
+ * null until the first load answers. False means the caller may not mint
33
+ * keys (anonymous, or a token without the right to) — the guide is still
34
+ * worth showing, so this is a fact rather than an error.
35
+ */
36
+ const canMint = ref<boolean | null>(null);
37
+
38
+ /** The secret, held in memory for exactly as long as the user is looking. */
39
+ const revealed = ref<S3KeyCreated | null>(null);
40
+
41
+ function messageOf(e: unknown): string {
42
+ const err = e as { message?: string; detail?: string } | null;
43
+ // The backend's own words beat a status line: "access keys are not
44
+ // available on this install" tells an operator what to fix; "503" does not.
45
+ if (err?.detail) {
46
+ try {
47
+ const parsed = JSON.parse(err.detail) as { error?: string };
48
+ if (parsed?.error) return parsed.error;
49
+ } catch {
50
+ /* not JSON — fall through */
51
+ }
52
+ }
53
+ return err?.message || String(e);
54
+ }
55
+
56
+ function statusOf(e: unknown): number | undefined {
57
+ return (e as { status?: number } | null)?.status;
58
+ }
59
+
60
+ async function load(): Promise<void> {
61
+ loading.value = true;
62
+ error.value = null;
63
+ try {
64
+ const body = await api.jsonFetch<{ keys: S3AccessKey[] } & S3Connection>(
65
+ url('/api/auth/s3-keys'),
66
+ );
67
+ keys.value = Array.isArray(body?.keys) ? body.keys : [];
68
+ connection.value = {
69
+ endpoint: body?.endpoint ?? '',
70
+ enabled: body?.enabled !== false,
71
+ path_style: body?.path_style !== false,
72
+ };
73
+ canMint.value = true;
74
+ } catch (e) {
75
+ keys.value = [];
76
+ const st = statusOf(e);
77
+ if (st === 401 || st === 403) {
78
+ canMint.value = false;
79
+ } else {
80
+ canMint.value = false;
81
+ error.value = messageOf(e);
82
+ }
83
+ } finally {
84
+ loading.value = false;
85
+ loaded.value = true;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Mint a key. The secret comes back once and is held in `revealed` until
91
+ * the caller dismisses it — there is no second chance, so the UI must not
92
+ * navigate away from it on its own.
93
+ */
94
+ async function create(req: S3KeyRequest): Promise<S3KeyCreated | null> {
95
+ error.value = null;
96
+ try {
97
+ const body = await api.jsonFetch<S3KeyCreated>(url('/api/auth/s3-keys'), {
98
+ method: 'POST',
99
+ headers: { 'Content-Type': 'application/json' },
100
+ body: JSON.stringify(req),
101
+ });
102
+ revealed.value = body;
103
+ if (body?.endpoint) {
104
+ connection.value = {
105
+ endpoint: body.endpoint,
106
+ enabled: body.enabled !== false,
107
+ path_style: body.path_style !== false,
108
+ };
109
+ }
110
+ await load();
111
+ return body;
112
+ } catch (e) {
113
+ error.value = messageOf(e);
114
+ return null;
115
+ }
116
+ }
117
+
118
+ async function setDisabled(id: number, disabled: boolean): Promise<void> {
119
+ error.value = null;
120
+ try {
121
+ await api.jsonFetch(url(`/api/auth/s3-keys/${id}/state`), {
122
+ method: 'POST',
123
+ headers: { 'Content-Type': 'application/json' },
124
+ body: JSON.stringify({ disabled }),
125
+ });
126
+ await load();
127
+ } catch (e) {
128
+ error.value = messageOf(e);
129
+ }
130
+ }
131
+
132
+ async function remove(id: number): Promise<void> {
133
+ error.value = null;
134
+ try {
135
+ await api.jsonFetch(url(`/api/auth/s3-keys/${id}`), { method: 'DELETE' });
136
+ // A revealed secret belonging to the key just revoked must go with it.
137
+ if (revealed.value?.key?.id === id) revealed.value = null;
138
+ await load();
139
+ } catch (e) {
140
+ error.value = messageOf(e);
141
+ }
142
+ }
143
+
144
+ function dismissSecret() {
145
+ revealed.value = null;
146
+ }
147
+
148
+ /**
149
+ * The key a guide should be rendered with: the one just minted, else the
150
+ * newest usable one. A guide showing a DISABLED key's id would produce a
151
+ * paste that authenticates as nothing.
152
+ */
153
+ const guideKey = computed<S3AccessKey | null>(() => {
154
+ if (revealed.value?.key) return revealed.value.key;
155
+ const usable = keys.value.filter((k) => !k.disabled_at);
156
+ if (!usable.length) return null;
157
+ return usable.reduce((a, b) => (a.created_at >= b.created_at ? a : b));
158
+ });
159
+
160
+ return {
161
+ keys,
162
+ connection,
163
+ loading,
164
+ loaded,
165
+ error,
166
+ canMint,
167
+ revealed,
168
+ guideKey,
169
+ load,
170
+ create,
171
+ setDisabled,
172
+ remove,
173
+ dismissSecret,
174
+ };
175
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * useSSHKeys — the one client for the SSH key surface.
3
+ *
4
+ * Same shape as useS3Keys and for the same reason: the panel, the guide and
5
+ * the buttons exist once in `packages/core`, and the desktop app and the web
6
+ * app mount the same component. A credential surface with two implementations
7
+ * eventually hands out access one of them cannot take back.
8
+ *
9
+ * ⚠ There is no secret to reveal here. The user pastes the PUBLIC half; the
10
+ * private key never touches filex, which is the entire point of preferring
11
+ * keys over a password on a file server.
12
+ */
13
+
14
+ import { computed, ref, shallowRef } from 'vue';
15
+ import type { ExplorerConfig } from '../types/ExplorerConfig';
16
+ import type { SSHConnection, SSHPublicKey } from '../types/SSHKeys';
17
+ import { connectionsBase } from './useConnections';
18
+ import { useFileApi } from './useFileApi';
19
+
20
+ export function useSSHKeys(config: ExplorerConfig) {
21
+ const api = useFileApi(config);
22
+ const base = connectionsBase(config);
23
+ const url = (path: string) => `${base}${path}`;
24
+
25
+ const keys = shallowRef<SSHPublicKey[]>([]);
26
+ const connection = ref<SSHConnection | null>(null);
27
+ const loading = ref(false);
28
+ const loaded = ref(false);
29
+ const error = ref<string | null>(null);
30
+ const canAdd = ref<boolean | null>(null);
31
+
32
+ function messageOf(e: unknown): string {
33
+ const err = e as { message?: string; detail?: string } | null;
34
+ if (err?.detail) {
35
+ try {
36
+ const parsed = JSON.parse(err.detail) as { error?: string };
37
+ if (parsed?.error) return parsed.error;
38
+ } catch {
39
+ /* not JSON */
40
+ }
41
+ }
42
+ return err?.message || String(e);
43
+ }
44
+
45
+ function statusOf(e: unknown): number | undefined {
46
+ return (e as { status?: number } | null)?.status;
47
+ }
48
+
49
+ async function load(): Promise<void> {
50
+ loading.value = true;
51
+ error.value = null;
52
+ try {
53
+ const body = await api.jsonFetch<{ keys: SSHPublicKey[] } & SSHConnection>(
54
+ url('/api/auth/ssh-keys'),
55
+ );
56
+ keys.value = Array.isArray(body?.keys) ? body.keys : [];
57
+ connection.value = {
58
+ enabled: body?.enabled !== false,
59
+ host: body?.host ?? '',
60
+ port: body?.port ?? 2022,
61
+ login: body?.login ?? '',
62
+ ftps: body?.ftps,
63
+ };
64
+ canAdd.value = true;
65
+ } catch (e) {
66
+ keys.value = [];
67
+ canAdd.value = false;
68
+ if (statusOf(e) !== 401 && statusOf(e) !== 403) error.value = messageOf(e);
69
+ } finally {
70
+ loading.value = false;
71
+ loaded.value = true;
72
+ }
73
+ }
74
+
75
+ async function add(key: string, name?: string): Promise<boolean> {
76
+ error.value = null;
77
+ try {
78
+ await api.jsonFetch(url('/api/auth/ssh-keys'), {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ key, name }),
82
+ });
83
+ await load();
84
+ return true;
85
+ } catch (e) {
86
+ error.value = messageOf(e);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ async function setDisabled(id: number, disabled: boolean): Promise<void> {
92
+ error.value = null;
93
+ try {
94
+ await api.jsonFetch(url(`/api/auth/ssh-keys/${id}/state`), {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({ disabled }),
98
+ });
99
+ await load();
100
+ } catch (e) {
101
+ error.value = messageOf(e);
102
+ }
103
+ }
104
+
105
+ async function remove(id: number): Promise<void> {
106
+ error.value = null;
107
+ try {
108
+ await api.jsonFetch(url(`/api/auth/ssh-keys/${id}`), { method: 'DELETE' });
109
+ await load();
110
+ } catch (e) {
111
+ error.value = messageOf(e);
112
+ }
113
+ }
114
+
115
+ /** True when at least one key can actually be used to sign in. */
116
+ const hasUsableKey = computed(() => keys.value.some((k) => !k.disabled_at));
117
+
118
+ return { keys, connection, loading, loaded, error, canAdd, hasUsableKey, load, add, setDisabled, remove };
119
+ }
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // The backend emits `thumb_url` as a ROOT-RELATIVE path ("/api/files/thumb/{id}").
4
4
  // A plain `<img src>` only works for the native same-origin SPA: an embedded
5
- // webcomponent resolves it against the HOST page's origin (work.brf.sh → 404)
5
+ // webcomponent resolves it against the HOST page's origin (work.example.com → 404)
6
6
  // and, even with the URL fixed, `<img>` cannot carry the bearer header a
7
7
  // proxied host (fishapp PWA) requires. So thumbs are fetched through the same
8
8
  // auth machinery as every API call (headers + credentials), cached as object
@@ -0,0 +1,121 @@
1
+ /**
2
+ * useTokens — the one client for the self-service API-token surface.
3
+ *
4
+ * It goes through `useFileApi`, the same auth plumbing the explorer uses, so
5
+ * the desktop app's function-token, the web app's cookie and an embedder's
6
+ * bearer all work here without this file knowing which is which.
7
+ *
8
+ * ⚠ `/api/tokens`, NOT `/api/admin/ai-tokens`. The admin route needs an admin;
9
+ * this one is open to every account and caps what it hands out to the caller's
10
+ * own role and grants. Pointing this at the admin route would put the panel on
11
+ * three surfaces and have it fail with 403 on two of them.
12
+ *
13
+ * ⚠ Every surface mounts THIS, not a copy — the standing rule applied to a
14
+ * credential surface, where a divergence would mean one surface minting
15
+ * tokens the other cannot see or revoke.
16
+ */
17
+
18
+ import { ref, shallowRef } from 'vue';
19
+ import type { ExplorerConfig } from '../types/ExplorerConfig';
20
+ import type { ApiToken, ApiTokenCreated, ApiTokenRequest } from '../types/Tokens';
21
+ import { connectionsBase } from './useConnections';
22
+ import { useFileApi } from './useFileApi';
23
+
24
+ export function useTokens(config: ExplorerConfig) {
25
+ const api = useFileApi(config);
26
+ const base = connectionsBase(config);
27
+ const url = (path: string) => `${base}${path}`;
28
+
29
+ const tokens = shallowRef<ApiToken[]>([]);
30
+ const loading = ref(false);
31
+ const loaded = ref(false);
32
+ const error = ref<string | null>(null);
33
+ /**
34
+ * null until the first load answers. False means the caller may not mint
35
+ * one (anonymous, or a share/proxy session) — the guide around it is still
36
+ * worth showing, so this is a fact rather than an error.
37
+ */
38
+ const canMint = ref<boolean | null>(null);
39
+
40
+ /** The secret, held in memory for exactly as long as the user is looking. */
41
+ const revealed = ref<ApiTokenCreated | null>(null);
42
+
43
+ function messageOf(e: unknown): string {
44
+ const err = e as { message?: string; detail?: string } | null;
45
+ // The backend's own words beat a status line: "scope 'admin' is not
46
+ // available here" tells the user what to change; "403" does not.
47
+ if (err?.detail) {
48
+ try {
49
+ const parsed = JSON.parse(err.detail) as { error?: string };
50
+ if (parsed?.error) return parsed.error;
51
+ } catch {
52
+ /* not JSON — fall through */
53
+ }
54
+ }
55
+ return err?.message || String(e);
56
+ }
57
+
58
+ function statusOf(e: unknown): number | undefined {
59
+ return (e as { status?: number } | null)?.status;
60
+ }
61
+
62
+ async function load(): Promise<void> {
63
+ loading.value = true;
64
+ error.value = null;
65
+ try {
66
+ const body = await api.jsonFetch<{ tokens: ApiToken[] }>(url('/api/tokens'));
67
+ tokens.value = Array.isArray(body?.tokens) ? body.tokens : [];
68
+ canMint.value = true;
69
+ } catch (e) {
70
+ tokens.value = [];
71
+ const st = statusOf(e);
72
+ canMint.value = false;
73
+ // 401/403 is "not for you", which the panel says in its own words. Any
74
+ // other status is a fault worth showing verbatim.
75
+ if (st !== 401 && st !== 403) error.value = messageOf(e);
76
+ } finally {
77
+ loading.value = false;
78
+ loaded.value = true;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Mint a token. The secret comes back once and is held in `revealed` until
84
+ * the caller dismisses it — there is no second chance, so the UI must not
85
+ * navigate away from it on its own.
86
+ */
87
+ async function create(req: ApiTokenRequest): Promise<ApiTokenCreated | null> {
88
+ error.value = null;
89
+ try {
90
+ const body = await api.jsonFetch<ApiTokenCreated>(url('/api/tokens'), {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify(req),
94
+ });
95
+ revealed.value = body;
96
+ await load();
97
+ return body;
98
+ } catch (e) {
99
+ error.value = messageOf(e);
100
+ return null;
101
+ }
102
+ }
103
+
104
+ async function remove(id: number): Promise<void> {
105
+ error.value = null;
106
+ try {
107
+ await api.jsonFetch(url(`/api/tokens/${id}`), { method: 'DELETE' });
108
+ // A revealed secret belonging to the token just revoked goes with it.
109
+ if (revealed.value?.row?.id === id) revealed.value = null;
110
+ await load();
111
+ } catch (e) {
112
+ error.value = messageOf(e);
113
+ }
114
+ }
115
+
116
+ function dismiss(): void {
117
+ revealed.value = null;
118
+ }
119
+
120
+ return { tokens, loading, loaded, error, canMint, revealed, load, create, remove, dismiss };
121
+ }