@thetechfossil/upfiles 1.0.0 → 1.0.2
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 +449 -443
- package/dist/index.d.mts +28 -0
- package/dist/index.d.ts +28 -0
- package/package.json +45 -43
package/README.md
CHANGED
|
@@ -1,443 +1,449 @@
|
|
|
1
|
-
# @thetechfossil/upfiles
|
|
2
|
-
|
|
3
|
-
Lightweight JavaScript client and React component to upload files via a presigned S3 flow. Similar to UploadThing-like DX.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
- `
|
|
146
|
-
-
|
|
147
|
-
- `
|
|
148
|
-
- `
|
|
149
|
-
- `
|
|
150
|
-
- `
|
|
151
|
-
- `
|
|
152
|
-
|
|
153
|
-
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
- `
|
|
161
|
-
- `
|
|
162
|
-
- `
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
-
|
|
235
|
-
- `
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
-
|
|
263
|
-
-
|
|
264
|
-
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
//
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
//
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
//
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
##
|
|
439
|
-
|
|
440
|
-
-
|
|
441
|
-
-
|
|
442
|
-
-
|
|
443
|
-
|
|
1
|
+
# @thetechfossil/upfiles
|
|
2
|
+
|
|
3
|
+
Lightweight JavaScript client and React component to upload files via a presigned S3 flow. Similar to UploadThing-like DX.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
📚 **Full documentation available at:** [https://ttf-upfiles-docs.netlify.app/](https://ttf-upfiles-docs.netlify.app/)
|
|
8
|
+
|
|
9
|
+
For detailed API reference, guides, examples, and more information, visit the documentation site.
|
|
10
|
+
|
|
11
|
+
- Client: get presigned URLs and PUT to S3
|
|
12
|
+
- React `<Uploader />`: drag/drop, progress, single/multiple, accepts/types, size limits
|
|
13
|
+
- React `<ProjectFilesWidget />`: list all files for a project (Plugin API), pick one, and optionally auto-save to your app
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Bun (recommended)
|
|
19
|
+
bun add @thetechfossil/upfiles
|
|
20
|
+
|
|
21
|
+
# npm
|
|
22
|
+
npm install @thetechfossil/upfiles
|
|
23
|
+
|
|
24
|
+
# pnpm
|
|
25
|
+
pnpm add @thetechfossil/upfiles
|
|
26
|
+
|
|
27
|
+
# yarn
|
|
28
|
+
yarn add @thetechfossil/upfiles
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { Uploader, ConnectProjectDialog } from '@thetechfossil/upfiles';
|
|
35
|
+
import { useState } from 'react';
|
|
36
|
+
|
|
37
|
+
export default function Page() {
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
const [apiKey, setApiKey] = useState<string | null>(null);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="space-y-6">
|
|
43
|
+
<button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
|
|
44
|
+
Connect to Upfiles
|
|
45
|
+
</button>
|
|
46
|
+
|
|
47
|
+
<Uploader
|
|
48
|
+
clientOptions={{
|
|
49
|
+
baseUrl: process.env.NEXT_PUBLIC_UPFILES_APP_URL!,
|
|
50
|
+
apiKey: apiKey ?? undefined, // set after connecting
|
|
51
|
+
apiKeyHeader: 'authorization',
|
|
52
|
+
}}
|
|
53
|
+
multiple
|
|
54
|
+
dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
|
|
55
|
+
buttonClassName="px-3 py-2 rounded bg-blue-600 text-white"
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<ConnectProjectDialog
|
|
59
|
+
baseUrl={process.env.NEXT_PUBLIC_UPFILES_APP_URL!}
|
|
60
|
+
open={open}
|
|
61
|
+
onOpenChange={setOpen}
|
|
62
|
+
onConnected={(key) => setApiKey(key)}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Server prerequisites
|
|
70
|
+
|
|
71
|
+
Your Upfiles app must expose the Plugin API presign endpoint:
|
|
72
|
+
|
|
73
|
+
- Method: `POST`
|
|
74
|
+
- Path: `/api/plugin/upload/presigned-url` (default; configurable)
|
|
75
|
+
- Body: `{ fileName, fileType, fileSize, folderPath?, projectId? }` (`fileSize` is required)
|
|
76
|
+
- Response: `{ presignedUrl, fileKey, publicUrl, apiKeyId?, projectId? }`
|
|
77
|
+
|
|
78
|
+
This repository already provides a Next.js route at `src/app/api/plugin/upload/presigned-url/route.ts`.
|
|
79
|
+
|
|
80
|
+
For plugins/packages calling across origins, authenticate using an API key header as documented below. CORS must allow your plugin origin.
|
|
81
|
+
|
|
82
|
+
## Basic usage (React)
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { Uploader } from '@thetechfossil/upfiles';
|
|
86
|
+
|
|
87
|
+
export default function Page() {
|
|
88
|
+
return (
|
|
89
|
+
<Uploader
|
|
90
|
+
multiple
|
|
91
|
+
accept={["image/*", "application/pdf"]}
|
|
92
|
+
maxFileSize={100 * 1024 * 1024}
|
|
93
|
+
onComplete={(files) => {
|
|
94
|
+
console.log('uploaded:', files);
|
|
95
|
+
}}
|
|
96
|
+
onError={(err) => {
|
|
97
|
+
console.error(err);
|
|
98
|
+
}}
|
|
99
|
+
dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
|
|
100
|
+
buttonClassName="px-3 py-2 rounded bg-blue-600 text-white"
|
|
101
|
+
// Optional: pass clientOptions for cross-origin + API key auth
|
|
102
|
+
// clientOptions={{
|
|
103
|
+
// baseUrl: 'https://YOUR_APP_HOST',
|
|
104
|
+
// // or presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url',
|
|
105
|
+
// apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
|
|
106
|
+
// apiKeyHeader: 'authorization', // or 'x-api-key' | 'x-up-api-key'
|
|
107
|
+
// }}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Basic usage (vanilla JS)
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { UpfilesClient } from '@thetechfossil/upfiles';
|
|
117
|
+
|
|
118
|
+
// Same-origin by default; only set these when needed:
|
|
119
|
+
const client = new UpfilesClient({
|
|
120
|
+
// presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url', // full override
|
|
121
|
+
// baseUrl: 'https://YOUR_APP_HOST', // used with presignPath
|
|
122
|
+
// presignPath: '/api/plugin/upload/presigned-url', // default
|
|
123
|
+
// withCredentials: true, // for session routes (not typical for Plugin API)
|
|
124
|
+
// Provide API key for Plugin API auth:
|
|
125
|
+
// apiKey: 'upk_...'
|
|
126
|
+
// apiKeyHeader: 'authorization' // or 'x-api-key' | 'x-up-api-key'
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
async function upload(file) {
|
|
130
|
+
const { presignedUrl, publicUrl } = await client.getPresignedUrl({
|
|
131
|
+
fileName: file.name,
|
|
132
|
+
fileType: file.type,
|
|
133
|
+
fileSize: file.size, // required
|
|
134
|
+
folderPath: 'my-plugin/uploads/',
|
|
135
|
+
});
|
|
136
|
+
const res = await client.uploadToS3(presignedUrl, file);
|
|
137
|
+
if (!res.ok) throw new Error('Upload failed');
|
|
138
|
+
return publicUrl;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## API
|
|
143
|
+
|
|
144
|
+
- `new UpfilesClient(options)`
|
|
145
|
+
- `presignUrl` (optional): full absolute URL to presign endpoint. Highest priority.
|
|
146
|
+
- `baseUrl` (optional): API base origin. Used with `presignPath`.
|
|
147
|
+
- `presignPath` (optional): relative path, default `/api/plugin/upload/presigned-url`.
|
|
148
|
+
- `headers` (optional): extra headers to send.
|
|
149
|
+
- `apiKey` (optional): API key value (e.g., `upk_...`).
|
|
150
|
+
- `apiKeyHeader` (optional): one of `'authorization' | 'x-api-key' | 'x-up-api-key'` (default `'authorization'`). If `'authorization'`, `Bearer` prefix is auto-added for `upk_...`.
|
|
151
|
+
- `withCredentials` (optional): `true` to send cookies.
|
|
152
|
+
- Defaults: same-origin requests to `/api/plugin/upload/presigned-url`.
|
|
153
|
+
- `client.getPresignedUrl({ fileName, fileType, fileSize, projectId?, folderPath? })`
|
|
154
|
+
- `client.uploadToS3(presignedUrl, file)`
|
|
155
|
+
- `client.upload(file, { projectId?, folderPath?, fetchThumbnails? })` → returns `{ publicUrl, fileKey, fileName, fileType, fileSize, projectId?, apiKeyId?, thumbnails? }`
|
|
156
|
+
- `client.getThumbnails(fileKey)` → returns `Thumbnail[]`
|
|
157
|
+
- `client.getProjectFiles({ folderPath? })` → returns `FileListItem[]`
|
|
158
|
+
|
|
159
|
+
- `<Uploader />` props
|
|
160
|
+
- `clientOptions` (optional): same as `UpfilesClient` options (only needed for cross-origin or auth customization)
|
|
161
|
+
- `multiple` (default true)
|
|
162
|
+
- `accept` (array of MIME patterns)
|
|
163
|
+
- `maxFileSize` (bytes, default 100MB)
|
|
164
|
+
- `maxFiles` (default 10)
|
|
165
|
+
- `fetchThumbnails` (boolean): if true, fetch thumbnails after upload via Plugin API
|
|
166
|
+
- `onComplete(files)` and `onError(error)`
|
|
167
|
+
- `className`, `buttonClassName`, `dropzoneClassName`
|
|
168
|
+
- `children`: custom dropzone inner content
|
|
169
|
+
|
|
170
|
+
### Thumbnails usage
|
|
171
|
+
|
|
172
|
+
Your Upfiles app exposes `GET /api/plugin/thumbnails?fileKey=...` for plugins. Enable `fetchThumbnails` to auto-fetch after upload, or call `client.getThumbnails(fileKey)` yourself.
|
|
173
|
+
|
|
174
|
+
Example with `client.upload`:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const result = await client.upload(file, { folderPath: 'my-plugin/', fetchThumbnails: true });
|
|
178
|
+
console.log(result.thumbnails);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### List project files (Plugin API)
|
|
182
|
+
|
|
183
|
+
Your Upfiles app exposes `GET /api/plugin/files?folderPath=...` for plugins (API key auth + CORS). The client can call it via `getProjectFiles`:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { UpfilesClient } from '@thetechfossil/upfiles';
|
|
187
|
+
|
|
188
|
+
const client = new UpfilesClient({
|
|
189
|
+
baseUrl: 'https://YOUR_APP_HOST',
|
|
190
|
+
apiKey: 'upk_...',
|
|
191
|
+
thumbnailsPath: '/api/plugin/thumbnails', // used to derive /api/plugin/files
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const files = await client.getProjectFiles({ folderPath: 'optional/subfolder' });
|
|
195
|
+
// [{ key, originalName, size, contentType, uploadedAt, url }, ...]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `<ProjectFilesWidget />`
|
|
199
|
+
|
|
200
|
+
Simple UI to fetch and select a file for the project tied to the API key. Emits the selection, and can optionally POST it to your backend.
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { ProjectFilesWidget } from '@thetechfossil/upfiles';
|
|
204
|
+
|
|
205
|
+
export default function PickFile() {
|
|
206
|
+
return (
|
|
207
|
+
<ProjectFilesWidget
|
|
208
|
+
clientOptions={{
|
|
209
|
+
baseUrl: 'https://YOUR_APP_HOST',
|
|
210
|
+
apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
|
|
211
|
+
apiKeyHeader: 'authorization',
|
|
212
|
+
thumbnailsPath: '/api/plugin/thumbnails',
|
|
213
|
+
}}
|
|
214
|
+
// Optional: filter within a folder
|
|
215
|
+
// folderPath="my-plugin/"
|
|
216
|
+
|
|
217
|
+
onSelect={(f) => {
|
|
218
|
+
// { name, key, url, size, contentType }
|
|
219
|
+
console.log('Selected:', f.name, f.key);
|
|
220
|
+
}}
|
|
221
|
+
|
|
222
|
+
// Optional: built-in save
|
|
223
|
+
// saveUrl="/api/files/save" // Your app route to persist selection
|
|
224
|
+
// onSave={async (f) => { await fetch('/api/files/save', { method: 'POST', body: JSON.stringify(f) }); }}
|
|
225
|
+
// onSaved={(result) => console.log('Saved!', result)}
|
|
226
|
+
/>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Notes
|
|
232
|
+
|
|
233
|
+
- Dev with Vite: configure a proxy so `/api/*` maps to your backend (e.g., `http://localhost:4000`), then `<Uploader />` works with no config.
|
|
234
|
+
- Ensure CORS where applicable (S3 bucket CORS; and set `PLUGIN_ALLOWED_ORIGINS` on your Upfiles app for cross-origin Plugin API calls).
|
|
235
|
+
- For cross-origin with cookies (NextAuth), set `withCredentials: true` and configure your server to allow credentials.
|
|
236
|
+
Plugin API typically uses API keys instead of cookies.
|
|
237
|
+
|
|
238
|
+
### Plugin endpoints provided by this repo
|
|
239
|
+
|
|
240
|
+
- `POST /api/plugin/upload/presigned-url` – generate presigned S3 URL for upload
|
|
241
|
+
- `GET /api/plugin/thumbnails?fileKey=...` – list thumbnails for a file
|
|
242
|
+
- `GET /api/plugin/files?folderPath=...` – list project files (API key scoped), optional folder prefix
|
|
243
|
+
|
|
244
|
+
## Connect a project to get an API key
|
|
245
|
+
|
|
246
|
+
This package ships a ready-made dialog built with Radix UI + Tailwind classes to help developers connect their project to your main Upfiles app and obtain an API key.
|
|
247
|
+
|
|
248
|
+
### Install
|
|
249
|
+
|
|
250
|
+
Your app must have Tailwind configured. The dialog uses Radix primitives (shadcn/ui compatible). Ensure the peer dependency is available in the host app:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
bun add @thetechfossil/upfiles @radix-ui/react-dialog
|
|
254
|
+
# or
|
|
255
|
+
npm install @thetechfossil/upfiles @radix-ui/react-dialog
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### `ConnectProjectDialog` (UI modal)
|
|
259
|
+
|
|
260
|
+
Props:
|
|
261
|
+
|
|
262
|
+
- `baseUrl`: your Upfiles app origin (e.g., `https://upfiles.example.com`).
|
|
263
|
+
- `open`, `onOpenChange`: control the dialog.
|
|
264
|
+
- `onConnected(apiKey, { projectId?, source })`: receives the plaintext API key and metadata.
|
|
265
|
+
|
|
266
|
+
It presents three options:
|
|
267
|
+
|
|
268
|
+
- Connect to existing project → lists projects via `GET /api/projects`, then creates a key via `POST /api/projects/:id/keys`.
|
|
269
|
+
- Add API key manually → text input to paste an existing key.
|
|
270
|
+
- Create new project → `POST /api/projects`, then `POST /api/projects/:id/keys`.
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import { useState } from 'react';
|
|
274
|
+
import { ConnectProjectDialog } from '@thetechfossil/upfiles';
|
|
275
|
+
|
|
276
|
+
export default function ConnectKeyButton() {
|
|
277
|
+
const [open, setOpen] = useState(false);
|
|
278
|
+
const [apiKey, setApiKey] = useState<string | null>(null);
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div>
|
|
282
|
+
<button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
|
|
283
|
+
Connect Upfiles
|
|
284
|
+
</button>
|
|
285
|
+
|
|
286
|
+
<ConnectProjectDialog
|
|
287
|
+
baseUrl={process.env.NEXT_PUBLIC_UPFILES_APP_URL!}
|
|
288
|
+
open={open}
|
|
289
|
+
onOpenChange={setOpen}
|
|
290
|
+
onConnected={(key, meta) => {
|
|
291
|
+
setApiKey(key);
|
|
292
|
+
// Persist the key in your own DB as needed (see below)
|
|
293
|
+
console.log('Connected via', meta.source, 'projectId', meta.projectId);
|
|
294
|
+
}}
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Store the key in your own DB
|
|
302
|
+
|
|
303
|
+
You control persistence. Example Next.js route handler to store an encrypted key for the current user:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
// app/api/integrations/upfiles/key/route.ts
|
|
307
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
308
|
+
import { prisma } from '@/lib/prisma';
|
|
309
|
+
import { getServerSession } from 'next-auth';
|
|
310
|
+
import { authOptions } from '@/lib/auth';
|
|
311
|
+
|
|
312
|
+
export async function POST(req: NextRequest) {
|
|
313
|
+
const session = await getServerSession(authOptions as any);
|
|
314
|
+
const userId = (session as any)?.user?.id as string | undefined;
|
|
315
|
+
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
316
|
+
|
|
317
|
+
const { apiKey } = await req.json();
|
|
318
|
+
if (!apiKey) return NextResponse.json({ error: 'apiKey required' }, { status: 400 });
|
|
319
|
+
|
|
320
|
+
// TODO: encrypt before saving
|
|
321
|
+
await prisma.user.update({ where: { id: userId }, data: { upfilesApiKey: apiKey } });
|
|
322
|
+
return NextResponse.json({ ok: true });
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
From `onConnected`, call this route to persist:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
await fetch('/api/integrations/upfiles/key', {
|
|
330
|
+
method: 'POST',
|
|
331
|
+
headers: { 'content-type': 'application/json' },
|
|
332
|
+
body: JSON.stringify({ apiKey })
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Utility functions (no UI)
|
|
337
|
+
|
|
338
|
+
If you prefer to build a custom UI, use the exported functions:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
import {
|
|
342
|
+
listProjects,
|
|
343
|
+
createProject,
|
|
344
|
+
connectProject,
|
|
345
|
+
addApiKeyManually,
|
|
346
|
+
createClientWithKey,
|
|
347
|
+
} from '@thetechfossil/upfiles';
|
|
348
|
+
|
|
349
|
+
const baseUrl = 'https://upfiles.example.com';
|
|
350
|
+
|
|
351
|
+
// 1) List existing projects (requires session cookie on baseUrl)
|
|
352
|
+
const projects = await listProjects({ baseUrl });
|
|
353
|
+
|
|
354
|
+
// 2) Create a new project and key
|
|
355
|
+
const { apiKey, projectId } = await createProject({ baseUrl, name: 'My App' });
|
|
356
|
+
|
|
357
|
+
// 3) Create a key for an existing project
|
|
358
|
+
const key2 = await connectProject({ baseUrl, projectId: 'proj_123' });
|
|
359
|
+
|
|
360
|
+
// 4) Manual
|
|
361
|
+
const manual = addApiKeyManually('upk_...');
|
|
362
|
+
|
|
363
|
+
// 5) Build an upload client with the key
|
|
364
|
+
const client = createClientWithKey(baseUrl, apiKey);
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### `<ImageManager />` props
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
type ImageManagerProps = {
|
|
371
|
+
open: boolean;
|
|
372
|
+
onOpenChange: (open: boolean) => void;
|
|
373
|
+
clientOptions: UpfilesClientOptions;
|
|
374
|
+
projectId?: string;
|
|
375
|
+
folderPath?: string;
|
|
376
|
+
title?: string;
|
|
377
|
+
description?: string;
|
|
378
|
+
className?: string;
|
|
379
|
+
gridClassName?: string;
|
|
380
|
+
onSelect: (image: {
|
|
381
|
+
url: string;
|
|
382
|
+
key: string;
|
|
383
|
+
originalName: string;
|
|
384
|
+
size: number;
|
|
385
|
+
contentType: string;
|
|
386
|
+
thumbnails?: { id: string; key: string; url: string; size: number; sizeType?: string }[];
|
|
387
|
+
}) => void;
|
|
388
|
+
onDelete?: (key: string) => Promise<void>;
|
|
389
|
+
deleteUrl?: string;
|
|
390
|
+
autoRecordToDb?: boolean;
|
|
391
|
+
fetchThumbnails?: boolean;
|
|
392
|
+
maxFileSize?: number;
|
|
393
|
+
maxFiles?: number;
|
|
394
|
+
mode?: 'full' | 'browse' | 'upload'; // default: 'full'
|
|
395
|
+
showDelete?: boolean; // default: true
|
|
396
|
+
};
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Image picker modal (Browse Mode)
|
|
400
|
+
|
|
401
|
+
You can use `ImageManager` in `browse` mode to let users select existing images without the upload tab:
|
|
402
|
+
|
|
403
|
+
```tsx
|
|
404
|
+
import { useState } from 'react';
|
|
405
|
+
import { ImageManager } from '@thetechfossil/upfiles';
|
|
406
|
+
|
|
407
|
+
export default function PickImage() {
|
|
408
|
+
const [open, setOpen] = useState(false);
|
|
409
|
+
return (
|
|
410
|
+
<div>
|
|
411
|
+
<button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
|
|
412
|
+
Pick image
|
|
413
|
+
</button>
|
|
414
|
+
<ImageManager
|
|
415
|
+
open={open}
|
|
416
|
+
onOpenChange={setOpen}
|
|
417
|
+
mode="browse"
|
|
418
|
+
clientOptions={{
|
|
419
|
+
baseUrl: process.env.NEXT_PUBLIC_UPFILES_APP_URL!,
|
|
420
|
+
apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
|
|
421
|
+
apiKeyHeader: 'authorization',
|
|
422
|
+
}}
|
|
423
|
+
// folderPath="my-plugin/images/" // optional
|
|
424
|
+
onSelect={async (img) => {
|
|
425
|
+
// Persist original image URL in your DB
|
|
426
|
+
await fetch('/api/media/save', {
|
|
427
|
+
method: 'POST',
|
|
428
|
+
headers: { 'content-type': 'application/json' },
|
|
429
|
+
body: JSON.stringify({ url: img.url, key: img.key }),
|
|
430
|
+
});
|
|
431
|
+
}}
|
|
432
|
+
/>
|
|
433
|
+
</div>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Reusing across multiple Next.js projects
|
|
439
|
+
|
|
440
|
+
- Install `@thetechfossil/upfiles` and `@radix-ui/react-dialog` in each project.
|
|
441
|
+
- Set an environment variable like `NEXT_PUBLIC_UPFILES_APP_URL` to point all consumers to your main Upfiles app.
|
|
442
|
+
- Each consumer stores its own copy of the API key in its DB, obtained via `ConnectProjectDialog`.
|
|
443
|
+
|
|
444
|
+
## Troubleshooting
|
|
445
|
+
|
|
446
|
+
- __401 Unauthorized when listing/creating projects__: The `projects` and `keys` endpoints require a signed-in session on the main app domain. Open the dialog from a page that can send cookies to `{baseUrl}` or run from the same origin; otherwise configure auth/CORS accordingly.
|
|
447
|
+
- __CORS errors calling main app__: Ensure the main app allows your consumer origin in CORS and, if you use cookies, sets the correct `Access-Control-Allow-Credentials` headers and SameSite attributes.
|
|
448
|
+
- __Uploads fail__: Verify S3 bucket CORS and that `POST /api/plugin/upload/presigned-url` is accessible from the consumer app.
|
|
449
|
+
- __API key header__: Default is `Authorization: Bearer upk_...`. If your server expects a different header, set `apiKeyHeader: 'x-api-key' | 'x-up-api-key'` in `UpfilesClient` options.
|
package/dist/index.d.mts
CHANGED
|
@@ -34,6 +34,19 @@ type FileListItem = {
|
|
|
34
34
|
uploadedAt: string;
|
|
35
35
|
url: string;
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Upfiles Client - JavaScript client for Upfiles Plugin API
|
|
39
|
+
*
|
|
40
|
+
* 📚 Full documentation: https://ttf-upfiles-docs.netlify.app/
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const client = new UpfilesClient({
|
|
45
|
+
* baseUrl: 'https://your-upfiles.example.com',
|
|
46
|
+
* apiKey: 'upk_...',
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
37
50
|
type UpfilesClientOptions = {
|
|
38
51
|
baseUrl?: string;
|
|
39
52
|
presignPath?: string;
|
|
@@ -44,6 +57,21 @@ type UpfilesClientOptions = {
|
|
|
44
57
|
apiKeyHeader?: 'authorization' | 'x-api-key' | 'x-up-api-key';
|
|
45
58
|
thumbnailsPath?: string;
|
|
46
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* UpfilesClient - Main client class for interacting with Upfiles Plugin API
|
|
62
|
+
*
|
|
63
|
+
* 📚 Documentation: https://ttf-upfiles-docs.netlify.app/
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const client = new UpfilesClient({
|
|
68
|
+
* baseUrl: 'https://your-upfiles.example.com',
|
|
69
|
+
* apiKey: 'upk_...',
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* const result = await client.upload(file, { folderPath: 'uploads/' });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
47
75
|
declare class UpfilesClient {
|
|
48
76
|
private baseUrl?;
|
|
49
77
|
private presignPath;
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,19 @@ type FileListItem = {
|
|
|
34
34
|
uploadedAt: string;
|
|
35
35
|
url: string;
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Upfiles Client - JavaScript client for Upfiles Plugin API
|
|
39
|
+
*
|
|
40
|
+
* 📚 Full documentation: https://ttf-upfiles-docs.netlify.app/
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const client = new UpfilesClient({
|
|
45
|
+
* baseUrl: 'https://your-upfiles.example.com',
|
|
46
|
+
* apiKey: 'upk_...',
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
37
50
|
type UpfilesClientOptions = {
|
|
38
51
|
baseUrl?: string;
|
|
39
52
|
presignPath?: string;
|
|
@@ -44,6 +57,21 @@ type UpfilesClientOptions = {
|
|
|
44
57
|
apiKeyHeader?: 'authorization' | 'x-api-key' | 'x-up-api-key';
|
|
45
58
|
thumbnailsPath?: string;
|
|
46
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* UpfilesClient - Main client class for interacting with Upfiles Plugin API
|
|
62
|
+
*
|
|
63
|
+
* 📚 Documentation: https://ttf-upfiles-docs.netlify.app/
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const client = new UpfilesClient({
|
|
68
|
+
* baseUrl: 'https://your-upfiles.example.com',
|
|
69
|
+
* apiKey: 'upk_...',
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* const result = await client.upload(file, { folderPath: 'uploads/' });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
47
75
|
declare class UpfilesClient {
|
|
48
76
|
private baseUrl?;
|
|
49
77
|
private presignPath;
|
package/package.json
CHANGED
|
@@ -1,43 +1,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@thetechfossil/upfiles",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Lightweight client and React components for Upfiles Plugin API (presigned S3)",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "UpFiles",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/your-org/upfiles.git"
|
|
10
|
-
},
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@thetechfossil/upfiles",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Lightweight client and React components for Upfiles Plugin API (presigned S3)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "UpFiles",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/your-org/upfiles.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://ttf-upfiles-docs.netlify.app/",
|
|
12
|
+
"documentation": "https://ttf-upfiles-docs.netlify.app/",
|
|
13
|
+
"main": "dist/index.cjs",
|
|
14
|
+
"module": "dist/index.mjs",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./dist/index.mjs",
|
|
19
|
+
"require": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
31
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
32
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
33
|
+
"prepublishOnly": "bun run build"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": ">=18",
|
|
37
|
+
"@radix-ui/react-dialog": ">=1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
41
|
+
"@types/react": "^19",
|
|
42
|
+
"tsup": "^8.3.0",
|
|
43
|
+
"typescript": "^5.6.2"
|
|
44
|
+
}
|
|
45
|
+
}
|