@xk2800/nextjs-template 0.7.1-beta.0 → 0.7.1-beta.10

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 (129) hide show
  1. package/README.md +32 -30
  2. package/components/auth/deviceCheck.tsx +47 -6
  3. package/components/auth/oneTap.tsx +5 -5
  4. package/components/dashboard/admin/adminUserConsentCard.tsx +79 -0
  5. package/components/dashboard/admin/adminUserDevicesCard.tsx +80 -0
  6. package/components/dashboard/admin/broadcastActions.tsx +148 -0
  7. package/components/dashboard/admin/confirmSendDialog.tsx +44 -0
  8. package/components/dashboard/admin/marketingEmailForm.tsx +115 -0
  9. package/components/dashboard/admin/marketingSection.tsx +67 -0
  10. package/components/dashboard/admin/subscribersCard.tsx +62 -0
  11. package/components/dashboard/admin/systemSettingsForm.tsx +33 -0
  12. package/components/dashboard/admin/systemSettingsSection.tsx +8 -2
  13. package/components/dashboard/errorFeedback.tsx +2 -4
  14. package/components/dashboard/feedbackDialog.tsx +4 -4
  15. package/components/dashboard/marketingUpdatesCard.tsx +62 -0
  16. package/components/dashboard/marketingUpdatesSection.tsx +10 -0
  17. package/components/site/cookie-banner.tsx +109 -7
  18. package/components/ui/alert-dialog.tsx +1 -1
  19. package/components/ui/badge.tsx +1 -1
  20. package/components/ui/card.tsx +1 -1
  21. package/components/ui/data-table.tsx +4 -1
  22. package/components/ui/dialog.tsx +1 -1
  23. package/components/ui/dropdown-menu.tsx +2 -2
  24. package/components/ui/file-upload.tsx +129 -7
  25. package/components/ui/select.tsx +189 -0
  26. package/components/ui/switch.tsx +2 -2
  27. package/dist/activity/logger.js +36 -0
  28. package/dist/activity/logger.js.map +1 -1
  29. package/dist/activity/logger.mjs +4 -4
  30. package/dist/activity/queries.d.mts +1 -1
  31. package/dist/activity/queries.d.ts +1 -1
  32. package/dist/activity/queries.js +36 -0
  33. package/dist/activity/queries.js.map +1 -1
  34. package/dist/activity/queries.mjs +3 -3
  35. package/dist/admin/queries.d.mts +19 -1
  36. package/dist/admin/queries.d.ts +19 -1
  37. package/dist/admin/queries.js +64 -0
  38. package/dist/admin/queries.js.map +1 -1
  39. package/dist/admin/queries.mjs +26 -5
  40. package/dist/admin/queries.mjs.map +1 -1
  41. package/dist/api/feedback.js +53 -2
  42. package/dist/api/feedback.js.map +1 -1
  43. package/dist/api/feedback.mjs +9 -8
  44. package/dist/api/feedback.mjs.map +1 -1
  45. package/dist/api/marketing.d.mts +36 -0
  46. package/dist/api/marketing.d.ts +36 -0
  47. package/dist/api/marketing.js +1357 -0
  48. package/dist/api/marketing.js.map +1 -0
  49. package/dist/api/marketing.mjs +170 -0
  50. package/dist/api/marketing.mjs.map +1 -0
  51. package/dist/auth/helpers.js +51 -0
  52. package/dist/auth/helpers.js.map +1 -1
  53. package/dist/auth/helpers.mjs +9 -64
  54. package/dist/auth/helpers.mjs.map +1 -1
  55. package/dist/auth/index.d.mts +2 -2
  56. package/dist/auth/index.d.ts +2 -2
  57. package/dist/auth/index.js +51 -0
  58. package/dist/auth/index.js.map +1 -1
  59. package/dist/auth/index.mjs +8 -7
  60. package/dist/{chunk-KAEA35OC.mjs → chunk-6MDQRT6U.mjs} +4 -4
  61. package/dist/{chunk-KAEA35OC.mjs.map → chunk-6MDQRT6U.mjs.map} +1 -1
  62. package/dist/{chunk-N6ECRCHY.mjs → chunk-6OAHCEDJ.mjs} +4 -3
  63. package/dist/chunk-6OAHCEDJ.mjs.map +1 -0
  64. package/dist/chunk-6Z3WUSDJ.mjs +62 -0
  65. package/dist/chunk-6Z3WUSDJ.mjs.map +1 -0
  66. package/dist/{chunk-VQ5B4MA7.mjs → chunk-7F2POZM6.mjs} +4 -4
  67. package/dist/{chunk-VQ5B4MA7.mjs.map → chunk-7F2POZM6.mjs.map} +1 -1
  68. package/dist/{chunk-XWVAJXAG.mjs → chunk-7QVIOQ47.mjs} +23 -19
  69. package/dist/chunk-7QVIOQ47.mjs.map +1 -0
  70. package/dist/chunk-C6RGN6QA.mjs +60 -0
  71. package/dist/chunk-C6RGN6QA.mjs.map +1 -0
  72. package/dist/chunk-OVKVD2HH.mjs +25 -0
  73. package/dist/chunk-OVKVD2HH.mjs.map +1 -0
  74. package/dist/{chunk-3MGEAGHU.mjs → chunk-P2G23JMY.mjs} +11 -6
  75. package/dist/chunk-P2G23JMY.mjs.map +1 -0
  76. package/dist/{chunk-TMJYAOC2.mjs → chunk-TKWNEKK6.mjs} +8 -2
  77. package/dist/chunk-TKWNEKK6.mjs.map +1 -0
  78. package/dist/{chunk-ZQCALTKB.mjs → chunk-TR5XFBTD.mjs} +34 -4
  79. package/dist/chunk-TR5XFBTD.mjs.map +1 -0
  80. package/dist/{chunk-WZDAMCUX.mjs → chunk-ZJMFE5B2.mjs} +4 -4
  81. package/dist/{chunk-WZDAMCUX.mjs.map → chunk-ZJMFE5B2.mjs.map} +1 -1
  82. package/dist/config/env.d.mts +2 -0
  83. package/dist/config/env.d.ts +2 -0
  84. package/dist/config/env.js +6 -0
  85. package/dist/config/env.js.map +1 -1
  86. package/dist/config/env.mjs +1 -1
  87. package/dist/db/index.d.mts +1 -1
  88. package/dist/db/index.d.ts +1 -1
  89. package/dist/db/index.js +36 -0
  90. package/dist/db/index.js.map +1 -1
  91. package/dist/db/index.mjs +3 -3
  92. package/dist/db/schema.d.mts +1 -1
  93. package/dist/db/schema.d.ts +1 -1
  94. package/dist/db/schema.js +30 -0
  95. package/dist/db/schema.js.map +1 -1
  96. package/dist/db/schema.mjs +1 -1
  97. package/dist/errors.js +36 -0
  98. package/dist/errors.js.map +1 -1
  99. package/dist/errors.mjs +4 -4
  100. package/dist/marketing/index.d.mts +35 -0
  101. package/dist/marketing/index.d.ts +35 -0
  102. package/dist/marketing/index.js +125 -0
  103. package/dist/marketing/index.js.map +1 -0
  104. package/dist/marketing/index.mjs +6 -0
  105. package/dist/marketing/index.mjs.map +1 -0
  106. package/dist/{schema-BNSBWNjK.d.mts → schema-CN_Y6EXv.d.mts} +149 -3
  107. package/dist/{schema-BNSBWNjK.d.ts → schema-CN_Y6EXv.d.ts} +149 -3
  108. package/dist/sessions/queries.js +36 -0
  109. package/dist/sessions/queries.js.map +1 -1
  110. package/dist/sessions/queries.mjs +3 -3
  111. package/dist/settings/queries.d.mts +4 -3
  112. package/dist/settings/queries.d.ts +4 -3
  113. package/dist/settings/queries.js +42 -0
  114. package/dist/settings/queries.js.map +1 -1
  115. package/dist/settings/queries.mjs +4 -4
  116. package/dist/users/queries.js +36 -0
  117. package/dist/users/queries.js.map +1 -1
  118. package/dist/users/queries.mjs +3 -3
  119. package/lib/cookie-consent.ts +72 -20
  120. package/lib/cookie-names.ts +47 -0
  121. package/lib/device-fingerprint.ts +66 -10
  122. package/lib/recalculate-columns.ts +2 -0
  123. package/package.json +28 -3
  124. package/styles/theme.css +17 -3
  125. package/dist/chunk-3MGEAGHU.mjs.map +0 -1
  126. package/dist/chunk-N6ECRCHY.mjs.map +0 -1
  127. package/dist/chunk-TMJYAOC2.mjs.map +0 -1
  128. package/dist/chunk-XWVAJXAG.mjs.map +0 -1
  129. package/dist/chunk-ZQCALTKB.mjs.map +0 -1
@@ -210,6 +210,7 @@ __export(schema_exports, {
210
210
  accounts: () => accounts,
211
211
  activityLogs: () => activityLogs,
212
212
  authThrottle: () => authThrottle,
213
+ consentLog: () => consentLog,
213
214
  deviceFingerprints: () => deviceFingerprints,
214
215
  errorRefs: () => errorRefs,
215
216
  passkeys: () => passkeys,
@@ -364,10 +365,32 @@ var deviceFingerprints = pgCore.pgTable("device_fingerprint", {
364
365
  userAgent: pgCore.text("userAgent"),
365
366
  country: pgCore.text("country"),
366
367
  city: pgCore.text("city"),
368
+ // Latest FingerprintJS signals for this device (lib/device-fingerprint.ts
369
+ // getDeviceSignals(): accessibility signals dropped, large values hashed),
370
+ // shown on the admin user page. Null for rows from before this column.
371
+ components: pgCore.jsonb("components").$type(),
367
372
  createdAt: pgCore.timestamp("createdAt").notNull().defaultNow()
368
373
  }, (table) => [
369
374
  pgCore.unique("device_fingerprint_user_visitor").on(table.userId, table.visitorId)
370
375
  ]);
376
+ var consentLog = pgCore.pgTable("consent_log", {
377
+ id: pgCore.text("id").notNull().primaryKey().$defaultFn(() => cuid2.createId()),
378
+ userId: pgCore.text("userId").notNull().references(() => users.id, { onDelete: "cascade" }),
379
+ // The cookie_consent value: "all", "necessary" or a comma list of purposes.
380
+ choice: pgCore.text("choice").notNull(),
381
+ // CONSENT_VERSION (lib/cookie-names.ts) at the time — which banner/policy
382
+ // wording the user agreed to.
383
+ version: pgCore.integer("version").notNull(),
384
+ // id of the user's record this one follows ("" for their first). Unique per
385
+ // user, so concurrent posts from several tabs that all saw the same latest
386
+ // record can't each insert a duplicate — one wins, the rest conflict. Works
387
+ // without transactions (neon-http has none). Null on rows from before it.
388
+ previousId: pgCore.text("previousId"),
389
+ createdAt: pgCore.timestamp("createdAt").notNull().defaultNow()
390
+ }, (table) => [
391
+ pgCore.index("consent_log_user_idx").on(table.userId, table.createdAt),
392
+ pgCore.unique("consent_log_chain").on(table.userId, table.previousId)
393
+ ]);
371
394
  var authThrottle = pgCore.pgTable("auth_throttle", {
372
395
  fingerprint: pgCore.text("fingerprint").notNull().primaryKey(),
373
396
  count: pgCore.integer("count").notNull().default(0),
@@ -397,6 +420,13 @@ var systemSettings = pgCore.pgTable("system_settings", {
397
420
  // Attach the error a user hit to feedback they send from it (see
398
421
  // lib/feedback-route.ts). Off = only what the user typed is sent.
399
422
  feedbackAttachErrors: pgCore.boolean("feedbackAttachErrors").notNull().default(true),
423
+ // GDPR/ePrivacy consent mode. On: cookie banner, FingerprintJS and One Tap
424
+ // only after "Accept all", device record erased on withdrawal. Off (no EU/UK
425
+ // users): no banner, treated as consented. Null = follow
426
+ // NEXT_PUBLIC_COOKIE_BANNER (nullable so migrating never flips a deployed
427
+ // banner on or off). proxy.ts mirrors the effective value into the
428
+ // consent_required cookie for the client (lib/cookie-consent.ts).
429
+ consentRequired: pgCore.boolean("consentRequired"),
400
430
  updatedAt: pgCore.timestamp("updatedAt").notNull().defaultNow(),
401
431
  // Who last changed a setting. This row doesn't "belong" to that admin the
402
432
  // way sessions/activityLogs belong to their user, so deleting that admin's
@@ -418,6 +448,12 @@ var envSchema = zod.z.object({
418
448
  PORT: zod.z.string().default("3000").transform(Number),
419
449
  DATABASE_URL: zod.z.string().default(""),
420
450
  RESEND_API_KEY: zod.z.string().default(""),
451
+ // Full access key for marketing broadcasts (admin Marketing email page).
452
+ // Blank = use RESEND_API_KEY.
453
+ RESEND_BROADCAST_API_KEY: zod.z.string().default(""),
454
+ // Resend segment users join from the dashboard "Email updates" toggle.
455
+ // Blank = toggle hidden.
456
+ RESEND_MARKETING_SEGMENT_ID: zod.z.string().default(""),
421
457
  // Sender for every transactional email. Must be on a domain verified in
422
458
  // your Resend account.
423
459
  EMAIL_FROM: zod.z.string().default("Acme <onboarding@xavierkhew.xyz>"),
@@ -557,6 +593,7 @@ var DEVICE_FINGERPRINT_HEADER = "x-device-fingerprint";
557
593
  var WINDOW_MS = 15 * 60 * 1e3;
558
594
  var MAX = 10;
559
595
  var MAX_IP = MAX * 3;
596
+ var RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
560
597
  function isOverLimit(row, now, max = MAX) {
561
598
  if (!row) return false;
562
599
  if (now - row.windowStart.getTime() >= WINDOW_MS) return false;
@@ -594,6 +631,18 @@ async function recordDeviceAttempt(fingerprint, meta) {
594
631
  } catch (error) {
595
632
  console.error("auth-throttle write failed", error);
596
633
  }
634
+ await pruneStaleRows(now);
635
+ }
636
+ var PRUNE_EVERY_MS = 60 * 60 * 1e3;
637
+ var lastPrune = 0;
638
+ async function pruneStaleRows(now = /* @__PURE__ */ new Date()) {
639
+ if (now.getTime() - lastPrune < PRUNE_EVERY_MS) return;
640
+ lastPrune = now.getTime();
641
+ try {
642
+ await db.delete(authThrottle).where(drizzleOrm.lt(authThrottle.lastAttemptAt, new Date(now.getTime() - RETENTION_MS)));
643
+ } catch (error) {
644
+ console.error("auth-throttle prune failed", error);
645
+ }
597
646
  }
598
647
  async function clearDeviceThrottle(fingerprint) {
599
648
  try {
@@ -616,6 +665,7 @@ function fallbackSettings() {
616
665
  authEnableOneTap: config.AUTH_ENABLE_ONE_TAP,
617
666
  enableSessionRevocation: config.ENABLE_SESSION_REVOCATION,
618
667
  feedbackAttachErrors: true,
668
+ consentRequired: null,
619
669
  updatedAt: /* @__PURE__ */ new Date(0),
620
670
  updatedBy: null
621
671
  };
@@ -665,6 +715,7 @@ async function getResend() {
665
715
  }
666
716
  return resend;
667
717
  }
718
+ config.RESEND_BROADCAST_API_KEY || config.RESEND_API_KEY;
668
719
 
669
720
  // server/auth.ts
670
721
  async function logFailedLogin(ctx) {
@@ -1056,7 +1107,7 @@ var auth = new Proxy({}, {
1056
1107
  var REF_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
1057
1108
  var REF_PATTERN = /\(ref: ([2-9A-HJ-NP-Z]{6})\)/;
1058
1109
  var DETAIL_MAX_LENGTH = 8e3;
1059
- var RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
1110
+ var RETENTION_MS2 = 30 * 24 * 60 * 60 * 1e3;
1060
1111
  function describeError(error) {
1061
1112
  if (error instanceof Error) return error.stack || `${error.name}: ${error.message}`;
1062
1113
  try {
@@ -1073,7 +1124,7 @@ function storeDetail(ref, message, error) {
1073
1124
  message,
1074
1125
  detail: describeError(error).slice(0, DETAIL_MAX_LENGTH)
1075
1126
  });
1076
- await db.delete(errorRefs).where(drizzleOrm.lt(errorRefs.createdAt, new Date(Date.now() - RETENTION_MS)));
1127
+ await db.delete(errorRefs).where(drizzleOrm.lt(errorRefs.createdAt, new Date(Date.now() - RETENTION_MS2)));
1077
1128
  } catch (storeError) {
1078
1129
  console.error(`[error ref ${ref}] could not store error detail`, storeError);
1079
1130
  }