@xk2800/nextjs-template 0.7.1-beta.1 → 0.7.1-beta.11

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 (127) 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 +3 -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 +120 -2
  25. package/components/ui/select.tsx +189 -0
  26. package/components/ui/switch.tsx +3 -3
  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.js +51 -0
  56. package/dist/auth/index.js.map +1 -1
  57. package/dist/auth/index.mjs +8 -7
  58. package/dist/{chunk-KAEA35OC.mjs → chunk-6MDQRT6U.mjs} +4 -4
  59. package/dist/{chunk-KAEA35OC.mjs.map → chunk-6MDQRT6U.mjs.map} +1 -1
  60. package/dist/{chunk-N6ECRCHY.mjs → chunk-6OAHCEDJ.mjs} +4 -3
  61. package/dist/chunk-6OAHCEDJ.mjs.map +1 -0
  62. package/dist/chunk-6Z3WUSDJ.mjs +62 -0
  63. package/dist/chunk-6Z3WUSDJ.mjs.map +1 -0
  64. package/dist/{chunk-VQ5B4MA7.mjs → chunk-7F2POZM6.mjs} +4 -4
  65. package/dist/{chunk-VQ5B4MA7.mjs.map → chunk-7F2POZM6.mjs.map} +1 -1
  66. package/dist/{chunk-XWVAJXAG.mjs → chunk-7QVIOQ47.mjs} +23 -19
  67. package/dist/chunk-7QVIOQ47.mjs.map +1 -0
  68. package/dist/chunk-C6RGN6QA.mjs +60 -0
  69. package/dist/chunk-C6RGN6QA.mjs.map +1 -0
  70. package/dist/chunk-OVKVD2HH.mjs +25 -0
  71. package/dist/chunk-OVKVD2HH.mjs.map +1 -0
  72. package/dist/{chunk-3MGEAGHU.mjs → chunk-P2G23JMY.mjs} +11 -6
  73. package/dist/chunk-P2G23JMY.mjs.map +1 -0
  74. package/dist/{chunk-TMJYAOC2.mjs → chunk-TKWNEKK6.mjs} +8 -2
  75. package/dist/chunk-TKWNEKK6.mjs.map +1 -0
  76. package/dist/{chunk-ZQCALTKB.mjs → chunk-TR5XFBTD.mjs} +34 -4
  77. package/dist/chunk-TR5XFBTD.mjs.map +1 -0
  78. package/dist/{chunk-WZDAMCUX.mjs → chunk-ZJMFE5B2.mjs} +4 -4
  79. package/dist/{chunk-WZDAMCUX.mjs.map → chunk-ZJMFE5B2.mjs.map} +1 -1
  80. package/dist/config/env.d.mts +2 -0
  81. package/dist/config/env.d.ts +2 -0
  82. package/dist/config/env.js +6 -0
  83. package/dist/config/env.js.map +1 -1
  84. package/dist/config/env.mjs +1 -1
  85. package/dist/db/index.d.mts +1 -1
  86. package/dist/db/index.d.ts +1 -1
  87. package/dist/db/index.js +36 -0
  88. package/dist/db/index.js.map +1 -1
  89. package/dist/db/index.mjs +3 -3
  90. package/dist/db/schema.d.mts +1 -1
  91. package/dist/db/schema.d.ts +1 -1
  92. package/dist/db/schema.js +30 -0
  93. package/dist/db/schema.js.map +1 -1
  94. package/dist/db/schema.mjs +1 -1
  95. package/dist/errors.js +36 -0
  96. package/dist/errors.js.map +1 -1
  97. package/dist/errors.mjs +4 -4
  98. package/dist/marketing/index.d.mts +35 -0
  99. package/dist/marketing/index.d.ts +35 -0
  100. package/dist/marketing/index.js +125 -0
  101. package/dist/marketing/index.js.map +1 -0
  102. package/dist/marketing/index.mjs +6 -0
  103. package/dist/marketing/index.mjs.map +1 -0
  104. package/dist/{schema-UuqxoOzS.d.mts → schema-CN_Y6EXv.d.mts} +148 -2
  105. package/dist/{schema-UuqxoOzS.d.ts → schema-CN_Y6EXv.d.ts} +148 -2
  106. package/dist/sessions/queries.js +36 -0
  107. package/dist/sessions/queries.js.map +1 -1
  108. package/dist/sessions/queries.mjs +3 -3
  109. package/dist/settings/queries.d.mts +4 -3
  110. package/dist/settings/queries.d.ts +4 -3
  111. package/dist/settings/queries.js +42 -0
  112. package/dist/settings/queries.js.map +1 -1
  113. package/dist/settings/queries.mjs +4 -4
  114. package/dist/users/queries.js +36 -0
  115. package/dist/users/queries.js.map +1 -1
  116. package/dist/users/queries.mjs +3 -3
  117. package/lib/cookie-consent.ts +72 -20
  118. package/lib/cookie-names.ts +47 -0
  119. package/lib/device-fingerprint.ts +66 -10
  120. package/lib/recalculate-columns.ts +2 -0
  121. package/package.json +28 -3
  122. package/styles/theme.css +23 -3
  123. package/dist/chunk-3MGEAGHU.mjs.map +0 -1
  124. package/dist/chunk-N6ECRCHY.mjs.map +0 -1
  125. package/dist/chunk-TMJYAOC2.mjs.map +0 -1
  126. package/dist/chunk-XWVAJXAG.mjs.map +0 -1
  127. package/dist/chunk-ZQCALTKB.mjs.map +0 -1
@@ -1,67 +1,12 @@
1
- import { auth } from '../chunk-XWVAJXAG.mjs';
2
- import '../chunk-VQ5B4MA7.mjs';
3
- import '../chunk-N6ECRCHY.mjs';
4
- import '../chunk-3MGEAGHU.mjs';
5
- import { db } from '../chunk-WZDAMCUX.mjs';
6
- import { users } from '../chunk-ZQCALTKB.mjs';
7
- import '../chunk-TMJYAOC2.mjs';
1
+ export { getCurrentSession, hasRole, normalizeCallbackUrl, requireAuth, requireRole } from '../chunk-6Z3WUSDJ.mjs';
2
+ import '../chunk-7QVIOQ47.mjs';
3
+ import '../chunk-7F2POZM6.mjs';
4
+ import '../chunk-6OAHCEDJ.mjs';
5
+ import '../chunk-P2G23JMY.mjs';
6
+ import '../chunk-OVKVD2HH.mjs';
7
+ import '../chunk-ZJMFE5B2.mjs';
8
+ import '../chunk-TR5XFBTD.mjs';
9
+ import '../chunk-TKWNEKK6.mjs';
8
10
  import '../chunk-3465SI5N.mjs';
9
- import 'server-only';
10
- import { cache } from 'react';
11
- import { headers } from 'next/headers';
12
- import { redirect } from 'next/navigation';
13
- import { eq } from 'drizzle-orm';
14
-
15
- var getCurrentSession = cache(async () => {
16
- const session = await auth.api.getSession({
17
- headers: await headers()
18
- });
19
- return session;
20
- });
21
- var requireAuth = cache(async () => {
22
- var _a;
23
- const session = await getCurrentSession();
24
- if (!(session == null ? void 0 : session.user)) {
25
- redirect("/login");
26
- }
27
- const user = await db.select().from(users).where(eq(users.id, session.user.id)).limit(1);
28
- if ((_a = user[0]) == null ? void 0 : _a.banned) {
29
- await auth.api.signOut({
30
- headers: await headers()
31
- });
32
- redirect("/login?error=account_banned");
33
- }
34
- return session;
35
- });
36
- function hasRole(userRole, requiredRole) {
37
- if (!userRole) return false;
38
- if (requiredRole === "admin") {
39
- return userRole === "admin";
40
- }
41
- return userRole === "user" || userRole === "admin" || userRole === "tester";
42
- }
43
- async function requireRole(requiredRole) {
44
- const session = await requireAuth();
45
- if (!hasRole(session.user.role, requiredRole)) {
46
- redirect("/dashboard");
47
- }
48
- return session;
49
- }
50
- function normalizeCallbackUrl(value) {
51
- if (typeof value !== "string" || !value.trim()) return "/dashboard";
52
- const candidate = value.trim();
53
- try {
54
- const appOrigin = new URL(process.env.NEXT_PUBLIC_APP_URL || process.env.BETTER_AUTH_URL || "http://localhost:3000").origin;
55
- const callbackUrl = new URL(candidate, appOrigin);
56
- if (callbackUrl.origin !== appOrigin) {
57
- return "/dashboard";
58
- }
59
- return `${callbackUrl.pathname}${callbackUrl.search}${callbackUrl.hash}` || "/dashboard";
60
- } catch (e) {
61
- return "/dashboard";
62
- }
63
- }
64
-
65
- export { getCurrentSession, hasRole, normalizeCallbackUrl, requireAuth, requireRole };
66
11
  //# sourceMappingURL=helpers.mjs.map
67
12
  //# sourceMappingURL=helpers.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/auth-helpers.ts"],"names":[],"mappings":";;;;;;;;;;;;;;AAgBO,IAAM,iBAAA,GAAoB,MAAM,YAAY;AACjD,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,GAAA,CAAI,UAAA,CAAW;AAAA,IACxC,OAAA,EAAS,MAAM,OAAA;AAAQ,GACxB,CAAA;AAED,EAAA,OAAO,OAAA;AACT,CAAC;AASM,IAAM,WAAA,GAAc,MAAM,YAAY;AA/B7C,EAAA,IAAA,EAAA;AAgCE,EAAA,MAAM,OAAA,GAAU,MAAM,iBAAA,EAAkB;AAExC,EAAA,IAAI,EAAC,mCAAS,IAAA,CAAA,EAAM;AAClB,IAAA,QAAA,CAAS,QAAQ,CAAA;AAAA,EACnB;AAGA,EAAA,MAAM,OAAO,MAAM,EAAA,CAChB,QAAO,CACP,IAAA,CAAK,KAAK,CAAA,CACV,KAAA,CAAM,EAAA,CAAG,KAAA,CAAM,IAAI,OAAA,CAAQ,IAAA,CAAK,EAAE,CAAC,CAAA,CACnC,MAAM,CAAC,CAAA;AAEV,EAAA,IAAA,CAAI,EAAA,GAAA,IAAA,CAAK,CAAC,CAAA,KAAN,IAAA,GAAA,MAAA,GAAA,EAAA,CAAS,MAAA,EAAQ;AAEnB,IAAA,MAAM,IAAA,CAAK,IAAI,OAAA,CAAQ;AAAA,MACrB,OAAA,EAAS,MAAM,OAAA;AAAQ,KACxB,CAAA;AACD,IAAA,QAAA,CAAS,6BAA6B,CAAA;AAAA,EACxC;AAEA,EAAA,OAAO,OAAA;AACT,CAAC;AAKM,SAAS,OAAA,CAAQ,UAAqC,YAAA,EAAyC;AACpG,EAAA,IAAI,CAAC,UAAU,OAAO,KAAA;AAEtB,EAAA,IAAI,iBAAiB,OAAA,EAAS;AAC5B,IAAA,OAAO,QAAA,KAAa,OAAA;AAAA,EACtB;AAGA,EAAA,OAAO,QAAA,KAAa,MAAA,IAAU,QAAA,KAAa,OAAA,IAAW,QAAA,KAAa,QAAA;AACrE;AAKA,eAAsB,YAAY,YAAA,EAAgC;AAChE,EAAA,MAAM,OAAA,GAAU,MAAM,WAAA,EAAY;AAElC,EAAA,IAAI,CAAC,OAAA,CAAQ,OAAA,CAAQ,IAAA,CAAK,IAAA,EAAM,YAAY,CAAA,EAAG;AAC7C,IAAA,QAAA,CAAS,YAAY,CAAA;AAAA,EACvB;AAEA,EAAA,OAAO,OAAA;AACT;AAQO,SAAS,qBAAqB,KAAA,EAAmC;AACtE,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,CAAC,KAAA,CAAM,IAAA,IAAQ,OAAO,YAAA;AAEvD,EAAA,MAAM,SAAA,GAAY,MAAM,IAAA,EAAK;AAE7B,EAAA,IAAI;AACF,IAAA,MAAM,SAAA,GAAY,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,uBAAuB,OAAA,CAAQ,GAAA,CAAI,eAAA,IAAmB,uBAAuB,CAAA,CAAE,MAAA;AACrH,IAAA,MAAM,WAAA,GAAc,IAAI,GAAA,CAAI,SAAA,EAAW,SAAS,CAAA;AAEhD,IAAA,IAAI,WAAA,CAAY,WAAW,SAAA,EAAW;AACpC,MAAA,OAAO,YAAA;AAAA,IACT;AAEA,IAAA,OAAO,CAAA,EAAG,YAAY,QAAQ,CAAA,EAAG,YAAY,MAAM,CAAA,EAAG,WAAA,CAAY,IAAI,CAAA,CAAA,IAAM,YAAA;AAAA,EAC9E,CAAA,CAAA,OAAQ,CAAA,EAAA;AACN,IAAA,OAAO,YAAA;AAAA,EACT;AACF","file":"helpers.mjs","sourcesContent":["import \"server-only\"\nimport { cache } from \"react\"\nimport { auth } from \"../server/auth\"\nimport { headers } from \"next/headers\"\nimport { redirect } from \"next/navigation\"\nimport { db } from \"../server/db\"\nimport { users } from \"../server/db/schema\"\nimport { eq } from \"drizzle-orm\"\n\n/**\n * Get current session (server-side only)\n * Returns session data or null if not authenticated\n *\n * Wrapped in React's cache() so the layout and page (both of which call\n * requireAuth per request) share one auth.api.getSession call instead of two.\n */\nexport const getCurrentSession = cache(async () => {\n const session = await auth.api.getSession({\n headers: await headers()\n })\n\n return session\n})\n\n/**\n * Require authentication - redirect to login if not authenticated\n * Use in server components/layouts that need protection\n *\n * Wrapped in cache() so the layout, page, and any admin layout that all call\n * requireAuth per request share one banned-status DB lookup instead of several.\n */\nexport const requireAuth = cache(async () => {\n const session = await getCurrentSession()\n\n if (!session?.user) {\n redirect('/login')\n }\n\n // Check if user is banned\n const user = await db\n .select()\n .from(users)\n .where(eq(users.id, session.user.id))\n .limit(1)\n\n if (user[0]?.banned) {\n // Sign out the user\n await auth.api.signOut({\n headers: await headers()\n })\n redirect('/login?error=account_banned')\n }\n\n return session\n})\n\n/**\n * Check if user has specific role\n */\nexport function hasRole(userRole: string | null | undefined, requiredRole: 'user' | 'admin'): boolean {\n if (!userRole) return false\n\n if (requiredRole === 'admin') {\n return userRole === 'admin'\n }\n\n // 'user' role check - every role ('user', 'admin', 'tester') has access\n return userRole === 'user' || userRole === 'admin' || userRole === 'tester'\n}\n\n/**\n * Require specific role - throw error if user doesn't have it\n */\nexport async function requireRole(requiredRole: 'user' | 'admin') {\n const session = await requireAuth()\n\n if (!hasRole(session.user.role, requiredRole)) {\n redirect('/dashboard')\n }\n\n return session\n}\n\n/**\n * Normalize a callbackUrl query param to a same-origin path before it's ever\n * passed to redirect() or an auth form's callbackURL. Guards against open\n * redirects (e.g. /login?callbackUrl=https://evil.com) — anything that\n * doesn't resolve to this app's own origin falls back to /dashboard.\n */\nexport function normalizeCallbackUrl(value?: string | string[]): string {\n if (typeof value !== 'string' || !value.trim()) return '/dashboard'\n\n const candidate = value.trim()\n\n try {\n const appOrigin = new URL(process.env.NEXT_PUBLIC_APP_URL || process.env.BETTER_AUTH_URL || 'http://localhost:3000').origin\n const callbackUrl = new URL(candidate, appOrigin)\n\n if (callbackUrl.origin !== appOrigin) {\n return '/dashboard'\n }\n\n return `${callbackUrl.pathname}${callbackUrl.search}${callbackUrl.hash}` || '/dashboard'\n } catch {\n return '/dashboard'\n }\n}\n\n"]}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"helpers.mjs"}
@@ -207,6 +207,7 @@ __export(schema_exports, {
207
207
  accounts: () => accounts,
208
208
  activityLogs: () => activityLogs,
209
209
  authThrottle: () => authThrottle,
210
+ consentLog: () => consentLog,
210
211
  deviceFingerprints: () => deviceFingerprints,
211
212
  errorRefs: () => errorRefs,
212
213
  passkeys: () => passkeys,
@@ -361,10 +362,32 @@ var deviceFingerprints = pgCore.pgTable("device_fingerprint", {
361
362
  userAgent: pgCore.text("userAgent"),
362
363
  country: pgCore.text("country"),
363
364
  city: pgCore.text("city"),
365
+ // Latest FingerprintJS signals for this device (lib/device-fingerprint.ts
366
+ // getDeviceSignals(): accessibility signals dropped, large values hashed),
367
+ // shown on the admin user page. Null for rows from before this column.
368
+ components: pgCore.jsonb("components").$type(),
364
369
  createdAt: pgCore.timestamp("createdAt").notNull().defaultNow()
365
370
  }, (table) => [
366
371
  pgCore.unique("device_fingerprint_user_visitor").on(table.userId, table.visitorId)
367
372
  ]);
373
+ var consentLog = pgCore.pgTable("consent_log", {
374
+ id: pgCore.text("id").notNull().primaryKey().$defaultFn(() => cuid2.createId()),
375
+ userId: pgCore.text("userId").notNull().references(() => users.id, { onDelete: "cascade" }),
376
+ // The cookie_consent value: "all", "necessary" or a comma list of purposes.
377
+ choice: pgCore.text("choice").notNull(),
378
+ // CONSENT_VERSION (lib/cookie-names.ts) at the time — which banner/policy
379
+ // wording the user agreed to.
380
+ version: pgCore.integer("version").notNull(),
381
+ // id of the user's record this one follows ("" for their first). Unique per
382
+ // user, so concurrent posts from several tabs that all saw the same latest
383
+ // record can't each insert a duplicate — one wins, the rest conflict. Works
384
+ // without transactions (neon-http has none). Null on rows from before it.
385
+ previousId: pgCore.text("previousId"),
386
+ createdAt: pgCore.timestamp("createdAt").notNull().defaultNow()
387
+ }, (table) => [
388
+ pgCore.index("consent_log_user_idx").on(table.userId, table.createdAt),
389
+ pgCore.unique("consent_log_chain").on(table.userId, table.previousId)
390
+ ]);
368
391
  var authThrottle = pgCore.pgTable("auth_throttle", {
369
392
  fingerprint: pgCore.text("fingerprint").notNull().primaryKey(),
370
393
  count: pgCore.integer("count").notNull().default(0),
@@ -394,6 +417,13 @@ var systemSettings = pgCore.pgTable("system_settings", {
394
417
  // Attach the error a user hit to feedback they send from it (see
395
418
  // lib/feedback-route.ts). Off = only what the user typed is sent.
396
419
  feedbackAttachErrors: pgCore.boolean("feedbackAttachErrors").notNull().default(true),
420
+ // GDPR/ePrivacy consent mode. On: cookie banner, FingerprintJS and One Tap
421
+ // only after "Accept all", device record erased on withdrawal. Off (no EU/UK
422
+ // users): no banner, treated as consented. Null = follow
423
+ // NEXT_PUBLIC_COOKIE_BANNER (nullable so migrating never flips a deployed
424
+ // banner on or off). proxy.ts mirrors the effective value into the
425
+ // consent_required cookie for the client (lib/cookie-consent.ts).
426
+ consentRequired: pgCore.boolean("consentRequired"),
397
427
  updatedAt: pgCore.timestamp("updatedAt").notNull().defaultNow(),
398
428
  // Who last changed a setting. This row doesn't "belong" to that admin the
399
429
  // way sessions/activityLogs belong to their user, so deleting that admin's
@@ -415,6 +445,12 @@ var envSchema = zod.z.object({
415
445
  PORT: zod.z.string().default("3000").transform(Number),
416
446
  DATABASE_URL: zod.z.string().default(""),
417
447
  RESEND_API_KEY: zod.z.string().default(""),
448
+ // Full access key for marketing broadcasts (admin Marketing email page).
449
+ // Blank = use RESEND_API_KEY.
450
+ RESEND_BROADCAST_API_KEY: zod.z.string().default(""),
451
+ // Resend segment users join from the dashboard "Email updates" toggle.
452
+ // Blank = toggle hidden.
453
+ RESEND_MARKETING_SEGMENT_ID: zod.z.string().default(""),
418
454
  // Sender for every transactional email. Must be on a domain verified in
419
455
  // your Resend account.
420
456
  EMAIL_FROM: zod.z.string().default("Acme <onboarding@xavierkhew.xyz>"),
@@ -554,6 +590,7 @@ var DEVICE_FINGERPRINT_HEADER = "x-device-fingerprint";
554
590
  var WINDOW_MS = 15 * 60 * 1e3;
555
591
  var MAX = 10;
556
592
  var MAX_IP = MAX * 3;
593
+ var RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
557
594
  function isOverLimit(row, now, max = MAX) {
558
595
  if (!row) return false;
559
596
  if (now - row.windowStart.getTime() >= WINDOW_MS) return false;
@@ -591,6 +628,18 @@ async function recordDeviceAttempt(fingerprint, meta) {
591
628
  } catch (error) {
592
629
  console.error("auth-throttle write failed", error);
593
630
  }
631
+ await pruneStaleRows(now);
632
+ }
633
+ var PRUNE_EVERY_MS = 60 * 60 * 1e3;
634
+ var lastPrune = 0;
635
+ async function pruneStaleRows(now = /* @__PURE__ */ new Date()) {
636
+ if (now.getTime() - lastPrune < PRUNE_EVERY_MS) return;
637
+ lastPrune = now.getTime();
638
+ try {
639
+ await db.delete(authThrottle).where(drizzleOrm.lt(authThrottle.lastAttemptAt, new Date(now.getTime() - RETENTION_MS)));
640
+ } catch (error) {
641
+ console.error("auth-throttle prune failed", error);
642
+ }
594
643
  }
595
644
  async function clearDeviceThrottle(fingerprint) {
596
645
  try {
@@ -613,6 +662,7 @@ function fallbackSettings() {
613
662
  authEnableOneTap: config.AUTH_ENABLE_ONE_TAP,
614
663
  enableSessionRevocation: config.ENABLE_SESSION_REVOCATION,
615
664
  feedbackAttachErrors: true,
665
+ consentRequired: null,
616
666
  updatedAt: /* @__PURE__ */ new Date(0),
617
667
  updatedBy: null
618
668
  };
@@ -662,6 +712,7 @@ async function getResend() {
662
712
  }
663
713
  return resend;
664
714
  }
715
+ config.RESEND_BROADCAST_API_KEY || config.RESEND_API_KEY;
665
716
 
666
717
  // server/auth.ts
667
718
  async function logFailedLogin(ctx) {