@thetechfossil/upfiles 0.3.0-beta → 0.3.1-beta

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 CHANGED
@@ -22,6 +22,44 @@ pnpm add @thetechfossil/upfiles
22
22
  yarn add @thetechfossil/upfiles
23
23
  ```
24
24
 
25
+ ## Quickstart
26
+
27
+ ```tsx
28
+ import { Uploader, ConnectProjectDialog } from '@thetechfossil/upfiles';
29
+ import { useState } from 'react';
30
+
31
+ export default function Page() {
32
+ const [open, setOpen] = useState(false);
33
+ const [apiKey, setApiKey] = useState<string | null>(null);
34
+
35
+ return (
36
+ <div className="space-y-6">
37
+ <button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
38
+ Connect to Upfiles
39
+ </button>
40
+
41
+ <Uploader
42
+ clientOptions={{
43
+ baseUrl: process.env.NEXT_PUBLIC_UPFILES_APP_URL!,
44
+ apiKey: apiKey ?? undefined, // set after connecting
45
+ apiKeyHeader: 'authorization',
46
+ }}
47
+ multiple
48
+ dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
49
+ buttonClassName="px-3 py-2 rounded bg-blue-600 text-white"
50
+ />
51
+
52
+ <ConnectProjectDialog
53
+ baseUrl={process.env.NEXT_PUBLIC_UPFILES_APP_URL!}
54
+ open={open}
55
+ onOpenChange={setOpen}
56
+ onConnected={(key) => setApiKey(key)}
57
+ />
58
+ </div>
59
+ );
60
+ }
61
+ ```
62
+
25
63
  ## Server prerequisites
26
64
 
27
65
  Your Upfiles app must expose the Plugin API presign endpoint:
@@ -196,3 +234,271 @@ export default function PickFile() {
196
234
  - `POST /api/plugin/upload/presigned-url` – generate presigned S3 URL for upload
197
235
  - `GET /api/plugin/thumbnails?fileKey=...` – list thumbnails for a file
198
236
  - `GET /api/plugin/files?folderPath=...` – list project files (API key scoped), optional folder prefix
237
+
238
+ ## Connect a project to get an API key
239
+
240
+ 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.
241
+
242
+ ### Install
243
+
244
+ Your app must have Tailwind configured. The dialog uses Radix primitives (shadcn/ui compatible). Ensure the peer dependency is available in the host app:
245
+
246
+ ```bash
247
+ bun add @thetechfossil/upfiles @radix-ui/react-dialog
248
+ # or
249
+ npm install @thetechfossil/upfiles @radix-ui/react-dialog
250
+ ```
251
+
252
+ ### `ConnectProjectDialog` (UI modal)
253
+
254
+ Props:
255
+
256
+ - `baseUrl`: your Upfiles app origin (e.g., `https://upfiles.example.com`).
257
+ - `open`, `onOpenChange`: control the dialog.
258
+ - `onConnected(apiKey, { projectId?, source })`: receives the plaintext API key and metadata.
259
+
260
+ It presents three options:
261
+
262
+ - Connect to existing project → lists projects via `GET /api/projects`, then creates a key via `POST /api/projects/:id/keys`.
263
+ - Add API key manually → text input to paste an existing key.
264
+ - Create new project → `POST /api/projects`, then `POST /api/projects/:id/keys`.
265
+
266
+ ```tsx
267
+ import { useState } from 'react';
268
+ import { ConnectProjectDialog } from '@thetechfossil/upfiles';
269
+
270
+ export default function ConnectKeyButton() {
271
+ const [open, setOpen] = useState(false);
272
+ const [apiKey, setApiKey] = useState<string | null>(null);
273
+
274
+ return (
275
+ <div>
276
+ <button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
277
+ Connect Upfiles
278
+ </button>
279
+
280
+ <ConnectProjectDialog
281
+ baseUrl={process.env.NEXT_PUBLIC_UPFILES_APP_URL!}
282
+ open={open}
283
+ onOpenChange={setOpen}
284
+ onConnected={(key, meta) => {
285
+ setApiKey(key);
286
+ // Persist the key in your own DB as needed (see below)
287
+ console.log('Connected via', meta.source, 'projectId', meta.projectId);
288
+ }}
289
+ />
290
+ </div>
291
+ );
292
+ }
293
+ ```
294
+
295
+ ### Store the key in your own DB
296
+
297
+ You control persistence. Example Next.js route handler to store an encrypted key for the current user:
298
+
299
+ ```ts
300
+ // app/api/integrations/upfiles/key/route.ts
301
+ import { NextRequest, NextResponse } from 'next/server';
302
+ import { prisma } from '@/lib/prisma';
303
+ import { getServerSession } from 'next-auth';
304
+ import { authOptions } from '@/lib/auth';
305
+
306
+ export async function POST(req: NextRequest) {
307
+ const session = await getServerSession(authOptions as any);
308
+ const userId = (session as any)?.user?.id as string | undefined;
309
+ if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
310
+
311
+ const { apiKey } = await req.json();
312
+ if (!apiKey) return NextResponse.json({ error: 'apiKey required' }, { status: 400 });
313
+
314
+ // TODO: encrypt before saving
315
+ await prisma.user.update({ where: { id: userId }, data: { upfilesApiKey: apiKey } });
316
+ return NextResponse.json({ ok: true });
317
+ }
318
+ ```
319
+
320
+ From `onConnected`, call this route to persist:
321
+
322
+ ```ts
323
+ await fetch('/api/integrations/upfiles/key', {
324
+ method: 'POST',
325
+ headers: { 'content-type': 'application/json' },
326
+ body: JSON.stringify({ apiKey })
327
+ });
328
+ ```
329
+
330
+ ### Utility functions (no UI)
331
+
332
+ If you prefer to build a custom UI, use the exported functions:
333
+
334
+ ```ts
335
+ import {
336
+ listProjects,
337
+ createProject,
338
+ connectProject,
339
+ addApiKeyManually,
340
+ createClientWithKey,
341
+ } from '@thetechfossil/upfiles';
342
+
343
+ const baseUrl = 'https://upfiles.example.com';
344
+
345
+ // 1) List existing projects (requires session cookie on baseUrl)
346
+ const projects = await listProjects({ baseUrl });
347
+
348
+ // 2) Create a new project and key
349
+ const { apiKey, projectId } = await createProject({ baseUrl, name: 'My App' });
350
+
351
+ // 3) Create a key for an existing project
352
+ const key2 = await connectProject({ baseUrl, projectId: 'proj_123' });
353
+
354
+ // 4) Manual
355
+ const manual = addApiKeyManually('upk_...');
356
+
357
+ // 5) Build an upload client with the key
358
+ const client = createClientWithKey(baseUrl, apiKey);
359
+ ```
360
+
361
+ ### Image picker modal (with thumbnails)
362
+
363
+ `ImagePickerDialog` lets users browse images (with thumbnails) and returns the original image URL for storage in your DB.
364
+
365
+ ```tsx
366
+ import { useState } from 'react';
367
+ import { ImagePickerDialog } from '@thetechfossil/upfiles';
368
+
369
+ export default function PickImage() {
370
+ const [open, setOpen] = useState(false);
371
+ return (
372
+ <div>
373
+ <button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
374
+ Pick image
375
+ </button>
376
+ <ImagePickerDialog
377
+ open={open}
378
+ onOpenChange={setOpen}
379
+ clientOptions={{
380
+ baseUrl: process.env.NEXT_PUBLIC_UPFILES_APP_URL!,
381
+ apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
382
+ apiKeyHeader: 'authorization',
383
+ }}
384
+ // folderPath="my-plugin/images/" // optional
385
+ onSelect={async (img) => {
386
+ // Persist original image URL in your DB
387
+ await fetch('/api/media/save', {
388
+ method: 'POST',
389
+ headers: { 'content-type': 'application/json' },
390
+ body: JSON.stringify({ url: img.url, key: img.key }),
391
+ });
392
+ }}
393
+ />
394
+ </div>
395
+ );
396
+ }
397
+ ```
398
+
399
+ ### Headless files utilities
400
+
401
+ If you want to build your own modal, these helpers fetch files and thumbnails:
402
+
403
+ ```ts
404
+ import {
405
+ fetchProjectFiles,
406
+ fetchFileThumbnails,
407
+ fetchProjectImagesWithThumbs,
408
+ } from '@thetechfossil/upfiles';
409
+
410
+ const clientOptions = {
411
+ baseUrl: process.env.NEXT_PUBLIC_UPFILES_APP_URL!,
412
+ apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
413
+ apiKeyHeader: 'authorization',
414
+ };
415
+
416
+ const files = await fetchProjectFiles(clientOptions, 'optional/folder/');
417
+ const thumbs = await fetchFileThumbnails(clientOptions, files[0].key);
418
+ const images = await fetchProjectImagesWithThumbs(clientOptions, { folderPath: 'optional/folder/', limitConcurrent: 6 });
419
+ ```
420
+
421
+ ### Connect using API key
422
+
423
+ If you already have an API key, use `connectUsingApiKey` to create an authenticated client:
424
+
425
+ ```ts
426
+ import { connectUsingApiKey } from '@thetechfossil/upfiles';
427
+
428
+ const { apiKey, client } = await connectUsingApiKey({
429
+ baseUrl: process.env.NEXT_PUBLIC_UPFILES_APP_URL!,
430
+ apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
431
+ apiKeyHeader: 'authorization',
432
+ });
433
+ ```
434
+
435
+ ## API Reference
436
+
437
+ ### Functions
438
+
439
+ - `listProjects({ baseUrl, fetch? }) => Promise<Project[]>`
440
+ - `createProject({ baseUrl, name, keyName?, fetch? }) => Promise<{ apiKey: string; projectId: string }>`
441
+ - `connectProject({ baseUrl, projectId, keyName?, fetch? }) => Promise<{ apiKey: string }>`
442
+ - `addApiKeyManually(apiKey) => { apiKey: string }`
443
+ - `createClientWithKey(baseUrl, apiKey) => UpfilesClient`
444
+ - `connectUsingApiKey({ baseUrl, apiKey, apiKeyHeader? }) => { apiKey: string; client: UpfilesClient }`
445
+ - `fetchProjectFiles(clientOptions, folderPath?) => Promise<FileListItem[]>`
446
+ - `fetchFileThumbnails(clientOptions, fileKey) => Promise<Thumbnail[]>`
447
+ - `fetchProjectImagesWithThumbs(clientOptions, { folderPath?, limitConcurrent? }) => Promise<(FileListItem & { thumbnails?: Thumbnail[] })[]>`
448
+
449
+ Types:
450
+
451
+ ```ts
452
+ type Project = { id: string; name: string; createdAt?: string };
453
+ ```
454
+
455
+ ### `<ConnectProjectDialog />` props
456
+
457
+ ```ts
458
+ type ConnectProjectDialogProps = {
459
+ baseUrl: string;
460
+ open: boolean;
461
+ onOpenChange: (open: boolean) => void;
462
+ onConnected: (apiKey: string, meta: { projectId?: string; source: 'existing' | 'new' | 'manual' }) => void;
463
+ fetchImpl?: typeof fetch; // optional override (testing)
464
+ title?: string;
465
+ description?: string;
466
+ className?: string;
467
+ };
468
+ ```
469
+
470
+ ### `<ImagePickerDialog />` props
471
+
472
+ ```ts
473
+ type ImagePickerDialogProps = {
474
+ open: boolean;
475
+ onOpenChange: (open: boolean) => void;
476
+ clientOptions: UpfilesClientOptions; // baseUrl + apiKey
477
+ folderPath?: string;
478
+ title?: string;
479
+ description?: string;
480
+ className?: string;
481
+ gridClassName?: string;
482
+ onSelect: (image: {
483
+ url: string;
484
+ key: string;
485
+ originalName: string;
486
+ size: number;
487
+ contentType: string;
488
+ thumbnails?: { id: string; key: string; url: string; size: number; sizeType?: string }[];
489
+ }) => void;
490
+ };
491
+ ```
492
+
493
+ ## Reusing across multiple Next.js projects
494
+
495
+ - Install `@thetechfossil/upfiles` and `@radix-ui/react-dialog` in each project.
496
+ - Set an environment variable like `NEXT_PUBLIC_UPFILES_APP_URL` to point all consumers to your main Upfiles app.
497
+ - Each consumer stores its own copy of the API key in its DB, obtained via `ConnectProjectDialog`.
498
+
499
+ ## Troubleshooting
500
+
501
+ - __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.
502
+ - __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.
503
+ - __Uploads fail__: Verify S3 bucket CORS and that `POST /api/plugin/upload/presigned-url` is accessible from the consumer app.
504
+ - __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
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
3
 
3
4
  type PresignResponse = {
4
5
  presignedUrl: string;
@@ -129,4 +130,91 @@ type ProjectFilesWidgetProps = {
129
130
  };
130
131
  declare const ProjectFilesWidget: React.FC<ProjectFilesWidgetProps>;
131
132
 
132
- export { type FileListItem, type PresignResponse, ProjectFilesWidget, type ProjectFilesWidgetProps, type Thumbnail, UpfilesClient, type UpfilesClientOptions, type UploadedFileResult, Uploader, type UploaderFile, type UploaderProps };
133
+ type ConnectProjectDialogProps = {
134
+ baseUrl: string;
135
+ open: boolean;
136
+ onOpenChange: (open: boolean) => void;
137
+ onConnected: (apiKey: string, meta: {
138
+ projectId?: string;
139
+ source: 'existing' | 'new' | 'manual';
140
+ }) => void;
141
+ fetchImpl?: typeof fetch;
142
+ title?: string;
143
+ description?: string;
144
+ className?: string;
145
+ };
146
+ declare function ConnectProjectDialog(props: ConnectProjectDialogProps): react_jsx_runtime.JSX.Element;
147
+
148
+ type ConnectOptions = {
149
+ baseUrl: string;
150
+ fetch?: typeof fetch;
151
+ };
152
+ type Project = {
153
+ id: string;
154
+ name: string;
155
+ createdAt?: string;
156
+ };
157
+ declare function listProjects(opts: ConnectOptions): Promise<Project[]>;
158
+ declare function createProject(opts: ConnectOptions & {
159
+ name: string;
160
+ keyName?: string;
161
+ }): Promise<{
162
+ apiKey: string;
163
+ projectId: string;
164
+ }>;
165
+ declare function connectProject(opts: ConnectOptions & {
166
+ projectId: string;
167
+ keyName?: string;
168
+ }): Promise<{
169
+ apiKey: string;
170
+ }>;
171
+ declare function addApiKeyManually(apiKey: string): {
172
+ apiKey: string;
173
+ };
174
+ declare function createClientWithKey(baseUrl: string, apiKey: string): UpfilesClient;
175
+ declare function connectUsingApiKey(opts: {
176
+ baseUrl: string;
177
+ apiKey: string;
178
+ apiKeyHeader?: 'authorization' | 'x-api-key' | 'x-up-api-key';
179
+ }): {
180
+ apiKey: string;
181
+ client: UpfilesClient;
182
+ };
183
+
184
+ declare function fetchProjectFiles(clientOptions: UpfilesClientOptions, folderPath?: string): Promise<FileListItem[]>;
185
+ declare function fetchFileThumbnails(clientOptions: UpfilesClientOptions, fileKey: string): Promise<Thumbnail[]>;
186
+ type ImageWithThumbs = FileListItem & {
187
+ thumbnails?: Thumbnail[];
188
+ };
189
+ declare function fetchProjectImagesWithThumbs(clientOptions: UpfilesClientOptions, opts?: {
190
+ folderPath?: string;
191
+ limitConcurrent?: number;
192
+ }): Promise<ImageWithThumbs[]>;
193
+
194
+ type ImagePickerDialogProps = {
195
+ open: boolean;
196
+ onOpenChange: (open: boolean) => void;
197
+ clientOptions: UpfilesClientOptions;
198
+ folderPath?: string;
199
+ title?: string;
200
+ description?: string;
201
+ className?: string;
202
+ gridClassName?: string;
203
+ onSelect: (image: {
204
+ url: string;
205
+ key: string;
206
+ originalName: string;
207
+ size: number;
208
+ contentType: string;
209
+ thumbnails?: {
210
+ id: string;
211
+ key: string;
212
+ url: string;
213
+ size: number;
214
+ sizeType?: string;
215
+ }[];
216
+ }) => void;
217
+ };
218
+ declare function ImagePickerDialog(props: ImagePickerDialogProps): react_jsx_runtime.JSX.Element;
219
+
220
+ export { type ConnectOptions, ConnectProjectDialog, type ConnectProjectDialogProps, type FileListItem, ImagePickerDialog, type ImagePickerDialogProps, type ImageWithThumbs, type PresignResponse, type Project, ProjectFilesWidget, type ProjectFilesWidgetProps, type Thumbnail, UpfilesClient, type UpfilesClientOptions, type UploadedFileResult, Uploader, type UploaderFile, type UploaderProps, addApiKeyManually, connectProject, connectUsingApiKey, createClientWithKey, createProject, fetchFileThumbnails, fetchProjectFiles, fetchProjectImagesWithThumbs, listProjects };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
3
 
3
4
  type PresignResponse = {
4
5
  presignedUrl: string;
@@ -129,4 +130,91 @@ type ProjectFilesWidgetProps = {
129
130
  };
130
131
  declare const ProjectFilesWidget: React.FC<ProjectFilesWidgetProps>;
131
132
 
132
- export { type FileListItem, type PresignResponse, ProjectFilesWidget, type ProjectFilesWidgetProps, type Thumbnail, UpfilesClient, type UpfilesClientOptions, type UploadedFileResult, Uploader, type UploaderFile, type UploaderProps };
133
+ type ConnectProjectDialogProps = {
134
+ baseUrl: string;
135
+ open: boolean;
136
+ onOpenChange: (open: boolean) => void;
137
+ onConnected: (apiKey: string, meta: {
138
+ projectId?: string;
139
+ source: 'existing' | 'new' | 'manual';
140
+ }) => void;
141
+ fetchImpl?: typeof fetch;
142
+ title?: string;
143
+ description?: string;
144
+ className?: string;
145
+ };
146
+ declare function ConnectProjectDialog(props: ConnectProjectDialogProps): react_jsx_runtime.JSX.Element;
147
+
148
+ type ConnectOptions = {
149
+ baseUrl: string;
150
+ fetch?: typeof fetch;
151
+ };
152
+ type Project = {
153
+ id: string;
154
+ name: string;
155
+ createdAt?: string;
156
+ };
157
+ declare function listProjects(opts: ConnectOptions): Promise<Project[]>;
158
+ declare function createProject(opts: ConnectOptions & {
159
+ name: string;
160
+ keyName?: string;
161
+ }): Promise<{
162
+ apiKey: string;
163
+ projectId: string;
164
+ }>;
165
+ declare function connectProject(opts: ConnectOptions & {
166
+ projectId: string;
167
+ keyName?: string;
168
+ }): Promise<{
169
+ apiKey: string;
170
+ }>;
171
+ declare function addApiKeyManually(apiKey: string): {
172
+ apiKey: string;
173
+ };
174
+ declare function createClientWithKey(baseUrl: string, apiKey: string): UpfilesClient;
175
+ declare function connectUsingApiKey(opts: {
176
+ baseUrl: string;
177
+ apiKey: string;
178
+ apiKeyHeader?: 'authorization' | 'x-api-key' | 'x-up-api-key';
179
+ }): {
180
+ apiKey: string;
181
+ client: UpfilesClient;
182
+ };
183
+
184
+ declare function fetchProjectFiles(clientOptions: UpfilesClientOptions, folderPath?: string): Promise<FileListItem[]>;
185
+ declare function fetchFileThumbnails(clientOptions: UpfilesClientOptions, fileKey: string): Promise<Thumbnail[]>;
186
+ type ImageWithThumbs = FileListItem & {
187
+ thumbnails?: Thumbnail[];
188
+ };
189
+ declare function fetchProjectImagesWithThumbs(clientOptions: UpfilesClientOptions, opts?: {
190
+ folderPath?: string;
191
+ limitConcurrent?: number;
192
+ }): Promise<ImageWithThumbs[]>;
193
+
194
+ type ImagePickerDialogProps = {
195
+ open: boolean;
196
+ onOpenChange: (open: boolean) => void;
197
+ clientOptions: UpfilesClientOptions;
198
+ folderPath?: string;
199
+ title?: string;
200
+ description?: string;
201
+ className?: string;
202
+ gridClassName?: string;
203
+ onSelect: (image: {
204
+ url: string;
205
+ key: string;
206
+ originalName: string;
207
+ size: number;
208
+ contentType: string;
209
+ thumbnails?: {
210
+ id: string;
211
+ key: string;
212
+ url: string;
213
+ size: number;
214
+ sizeType?: string;
215
+ }[];
216
+ }) => void;
217
+ };
218
+ declare function ImagePickerDialog(props: ImagePickerDialogProps): react_jsx_runtime.JSX.Element;
219
+
220
+ export { type ConnectOptions, ConnectProjectDialog, type ConnectProjectDialogProps, type FileListItem, ImagePickerDialog, type ImagePickerDialogProps, type ImageWithThumbs, type PresignResponse, type Project, ProjectFilesWidget, type ProjectFilesWidgetProps, type Thumbnail, UpfilesClient, type UpfilesClientOptions, type UploadedFileResult, Uploader, type UploaderFile, type UploaderProps, addApiKeyManually, connectProject, connectUsingApiKey, createClientWithKey, createProject, fetchFileThumbnails, fetchProjectFiles, fetchProjectImagesWithThumbs, listProjects };