@flaredev/cli 0.11.0 → 0.12.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 (45) hide show
  1. package/dist/index.js +4777 -3372
  2. package/package.json +2 -2
  3. package/templates/app/.dev.vars.example +20 -0
  4. package/templates/app/app/dashboard/import-actions.ts +2 -2
  5. package/templates/app/app/dashboard/invitations/actions.ts +62 -0
  6. package/templates/app/app/dashboard/invitations/page.tsx +121 -0
  7. package/templates/app/app/invite/[token]/route.ts +28 -0
  8. package/templates/app/app/sign-up/page.tsx +61 -42
  9. package/templates/app/components/dashboard/fields/file-field.tsx +132 -12
  10. package/templates/app/components/dashboard/invite-form.tsx +86 -0
  11. package/templates/app/components/dashboard/record-detail.tsx +13 -2
  12. package/templates/app/components/dashboard/resource-form-page.tsx +9 -1
  13. package/templates/app/components/dashboard/resource-form.tsx +3 -1
  14. package/templates/app/components/dashboard/resource-table.tsx +7 -1
  15. package/templates/app/lib/api.ts +41 -3
  16. package/templates/app/lib/auth-config.ts +60 -38
  17. package/templates/app/lib/auth.ts +67 -2
  18. package/templates/app/lib/dashboard-nav.ts +5 -0
  19. package/templates/app/lib/invitations.ts +142 -0
  20. package/templates/app/lib/rate-limit.ts +71 -0
  21. package/templates/app/lib/resource/handlers.ts +9 -2
  22. package/templates/app/lib/resource/http.ts +24 -13
  23. package/templates/app/lib/resource/store.ts +79 -27
  24. package/templates/app/lib/storage/cloudinary.ts +146 -0
  25. package/templates/app/lib/storage/uploadthing.ts +122 -0
  26. package/templates/app/lib/storage.ts +72 -6
  27. package/templates/app/lib/user-titles.ts +52 -0
  28. package/templates/app/worker/index.ts +4 -1
  29. package/templates/app/wrangler.jsonc +11 -0
  30. package/templates/locks/cloudflare/package-lock.json +570 -690
  31. package/templates/locks/cloudflare/pnpm-lock.yaml +1061 -1091
  32. package/templates/locks/cloudflare/yarn.lock +363 -381
  33. package/templates/locks/next/package-lock.json +283 -243
  34. package/templates/locks/next/pnpm-lock.yaml +890 -870
  35. package/templates/locks/next/yarn.lock +338 -295
  36. package/templates/next/README.md +35 -3
  37. package/templates/next/lib/auth.ts +65 -2
  38. package/templates/next/lib/dashboard.ts +3 -1
  39. package/templates/next/lib/invitations.ts +119 -0
  40. package/templates/next/lib/rate-limit.ts +83 -0
  41. package/templates/next/lib/resource/prisma-rows.ts +52 -9
  42. package/templates/next/lib/storage/disk.ts +128 -0
  43. package/templates/next/lib/storage/r2.ts +86 -0
  44. package/templates/next/lib/storage.ts +83 -66
  45. package/templates/next/lib/user-titles.ts +49 -0
@@ -1,12 +1,35 @@
1
1
  import { env } from "cloudflare:workers";
2
- import { createObjectKey, createStorage } from "@flaredev/core";
2
+ import { createObjectKey, createStorage, type StorageBucket } from "@flaredev/core";
3
+ import { cloudinaryBucket } from "./storage/cloudinary";
4
+ import { uploadThingBucket } from "./storage/uploadthing";
3
5
 
4
6
  /**
5
- * File storage on the `STORAGE` R2 bucket.
7
+ * Where uploaded files go.
6
8
  *
7
- * `createUploadUrl` / `createReadUrl` return short-lived signed URLs redeemed by
8
- * `app/api/storage/route.ts`. A URL grants access to whoever holds it, so check
9
- * that the current user may upload or view the object before issuing one.
9
+ * The default is the `STORAGE` R2 bucket this app is already bound to: no account to
10
+ * create, no keys to copy, and Wrangler simulates it locally as a directory. Set
11
+ * `STORAGE_DRIVER` to send files to a content delivery network instead.
12
+ *
13
+ * r2 The bound bucket. The default, and nothing to configure.
14
+ * cloudinary A CDN with an image pipeline. Serves from res.cloudinary.com.
15
+ * uploadthing A CDN with an uploader. Serves from <app>.ufs.sh.
16
+ *
17
+ * The drivers are in `lib/storage/`, in your repository, and are ordinary files you can
18
+ * read and change. Adding one means writing `get`, `put` and `delete`.
19
+ *
20
+ * `disk` exists on the Next.js stack and not here: a Worker has no filesystem to write
21
+ * to, and the bound bucket already covers what it is for.
22
+ *
23
+ * ## Private by default, and what breaks that
24
+ *
25
+ * With `r2`, a read URL is a short-lived signed token redeemed by
26
+ * `app/api/storage/route.ts`, so whoever issued it decided the viewer was allowed it.
27
+ *
28
+ * `cloudinary` and `uploadthing` are content delivery networks, and serving through
29
+ * this Worker would throw away the thing you are paying them for. So they hand back
30
+ * their own URL, which **anyone with the link can read, forever**. That is the normal
31
+ * way to use them and the wrong way to store a medical record. Set
32
+ * `STORAGE_PUBLIC=false` to keep every read signed and proxied.
10
33
  *
11
34
  * const { url } = await storage.createUploadUrl({
12
35
  * key: createObjectKey(file.name, `users/${user.id}`),
@@ -17,8 +40,51 @@ import { createObjectKey, createStorage } from "@flaredev/core";
17
40
  *
18
41
  * const src = await storage.createReadUrl({ key });
19
42
  */
43
+
44
+ type StorageDriver = "r2" | "cloudinary" | "uploadthing";
45
+
46
+ const DRIVERS: StorageDriver[] = ["r2", "cloudinary", "uploadthing"];
47
+
48
+ const vars = env as unknown as Record<string, string | undefined>;
49
+
50
+ const configured = (vars.STORAGE_DRIVER ?? "r2").trim().toLowerCase();
51
+ if (!DRIVERS.includes(configured as StorageDriver)) {
52
+ throw new Error(`Unknown STORAGE_DRIVER "${configured}". One of: ${DRIVERS.join(", ")}.`);
53
+ }
54
+ const driver = configured as StorageDriver;
55
+
56
+ /** Required configuration, named in the error rather than left as `undefined`. */
57
+ function required(name: string): string {
58
+ const value = vars[name];
59
+ if (!value) throw new Error(`STORAGE_DRIVER is "${driver}" and needs ${name}, which is not set. Add it with: wrangler secret put ${name}`);
60
+ return value;
61
+ }
62
+
63
+ /** Public delivery is the point of a CDN, so it is on unless it is turned off. */
64
+ const publicDelivery = vars.STORAGE_PUBLIC !== "false";
65
+
66
+ function selectBucket(): StorageBucket {
67
+ switch (driver) {
68
+ case "cloudinary":
69
+ return cloudinaryBucket({
70
+ cloudName: required("CLOUDINARY_CLOUD_NAME"),
71
+ apiKey: required("CLOUDINARY_API_KEY"),
72
+ apiSecret: required("CLOUDINARY_API_SECRET"),
73
+ publicDelivery,
74
+ });
75
+ case "uploadthing":
76
+ return uploadThingBucket({
77
+ token: required("UPLOADTHING_TOKEN"),
78
+ appId: required("UPLOADTHING_APP_ID"),
79
+ publicDelivery,
80
+ });
81
+ case "r2":
82
+ return env.STORAGE;
83
+ }
84
+ }
85
+
20
86
  export const storage = createStorage({
21
- bucket: env.STORAGE,
87
+ bucket: selectBucket(),
22
88
  // Signing keys are derived from the auth secret with a storage-specific label.
23
89
  secret: env.BETTER_AUTH_SECRET,
24
90
  });
@@ -0,0 +1,52 @@
1
+ // This file is yours.
2
+ //
3
+ // Flare copies the code that turns a resource descriptor into a working endpoint into
4
+ // your app rather than hiding it in node_modules, for the same reason shadcn/ui copies
5
+ // a component: you cannot fix, read or reason about what you cannot see.
6
+ //
7
+ // `flare diff` shows how your copy differs from the version Flare ships, and
8
+ // `flare update` applies the parts you choose. Neither runs unless you ask.
9
+
10
+ /**
11
+ * Names for the accounts a `belongsTo("User")` field points at.
12
+ *
13
+ * Better Auth owns the account table, so it is not a Flare resource and has no store to
14
+ * ask — but the admin still has to show "Ada Lovelace" where the column holds a uuid.
15
+ * This is the one place that reads it, and it reads two columns and no more.
16
+ *
17
+ * An account may have no name (a passkey-only sign-up, say), so the email stands in. It
18
+ * is the same fallback `/api/users` uses, so a record reads the same in the table, on
19
+ * the detail page and in the picker.
20
+ */
21
+ import { inArray } from "drizzle-orm";
22
+ import { getDb } from "@/db";
23
+ import { user } from "@/db/auth-schema";
24
+
25
+ /** What the admin needs to treat Better Auth's account table as a relation target. */
26
+ export const AUTH_USER_RELATION = {
27
+ name: "User",
28
+ label: "User",
29
+ pluralLabel: "Users",
30
+ slug: "users",
31
+ titleField: "name",
32
+ } as const;
33
+
34
+ /** id → display name, for the ids that exist. Unknown ids are simply absent. */
35
+ export async function userTitles(ids: string[]): Promise<Record<string, string>> {
36
+ const wanted = [...new Set(ids.filter(Boolean))];
37
+ if (wanted.length === 0) return {};
38
+ const rows = await getDb().select({ id: user.id, name: user.name, email: user.email }).from(user).where(inArray(user.id, wanted));
39
+ return Object.fromEntries(rows.map((row) => [row.id, row.name?.trim() || row.email]));
40
+ }
41
+
42
+ /**
43
+ * Whether anybody has an account yet.
44
+ *
45
+ * Asked by the sign-up page: the gate in lib/auth.ts always lets the *first* account
46
+ * through, whatever `authConfig.signup` says, because an app nobody can sign into cannot
47
+ * be set up. So "sign-up is closed" is only true once somebody exists.
48
+ */
49
+ export async function anyAccountExists(): Promise<boolean> {
50
+ const [first] = await getDb().select({ id: user.id }).from(user).limit(1);
51
+ return first !== undefined;
52
+ }
@@ -1,4 +1,5 @@
1
1
  import app from "vinext/server/app-router-entry";
2
+ import { RateLimitCounter } from "@flaredev/core/rate-limit/server";
2
3
  import { RealtimeChannel, handleRealtimeUpgrade } from "@flaredev/core/realtime/server";
3
4
  import { SecurityMonitor } from "@flaredev/core/security/server";
4
5
  import { authorizeRealtime } from "../lib/realtime";
@@ -9,12 +10,14 @@ import { protect } from "../lib/security";
9
10
  // the `cloudflare:workers` module.
10
11
 
11
12
  // Durable Object classes must be exported from the Worker's main module.
12
- export { RealtimeChannel, SecurityMonitor };
13
+ export { RateLimitCounter, RealtimeChannel, SecurityMonitor };
13
14
 
14
15
  export interface AppEnv {
15
16
  ASSETS: Fetcher;
16
17
  DB: D1Database;
17
18
  FLARE_REALTIME: DurableObjectNamespace<RealtimeChannel>;
19
+ /** Counters for the rate limits a descriptor declares (see lib/rate-limit.ts). */
20
+ FLARE_RATE_LIMIT: DurableObjectNamespace<RateLimitCounter>;
18
21
  }
19
22
 
20
23
  export interface AppContext {
@@ -16,6 +16,13 @@
16
16
  // Object instance created on demand; wrangler provisions nothing upfront.
17
17
  "name": "FLARE_REALTIME",
18
18
  "class_name": "RealtimeChannel"
19
+ },
20
+ {
21
+ // Rate limit counters (see lib/rate-limit.ts). One instance per resource and
22
+ // action, holding a count per caller — so a limited endpoint costs one object
23
+ // request, not one per person calling it.
24
+ "name": "FLARE_RATE_LIMIT",
25
+ "class_name": "RateLimitCounter"
19
26
  }
20
27
  ]
21
28
  },
@@ -23,6 +30,10 @@
23
30
  {
24
31
  "tag": "v1",
25
32
  "new_sqlite_classes": ["RealtimeChannel"]
33
+ },
34
+ {
35
+ "tag": "v2",
36
+ "new_sqlite_classes": ["RateLimitCounter"]
26
37
  }
27
38
  ],
28
39
  "d1_databases": [