@xk2800/nextjs-template 0.6.1-beta.3 → 0.7.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 (105) hide show
  1. package/README.md +56 -321
  2. package/components/dashboard/admin/adminUsersTable.tsx +36 -2
  3. package/components/dashboard/admin/feedbackTestForm.tsx +55 -0
  4. package/components/dashboard/admin/systemSettingsForm.tsx +28 -0
  5. package/components/dashboard/appSidebar.tsx +4 -4
  6. package/components/dashboard/deleteAccountCard.tsx +137 -0
  7. package/components/dashboard/errorFeedback.tsx +65 -0
  8. package/components/dashboard/feedbackDialog.tsx +160 -0
  9. package/components/dashboard/installAppCard.tsx +187 -0
  10. package/components/site/cookie-banner.tsx +57 -0
  11. package/components/ui/button.tsx +1 -1
  12. package/components/ui/file-upload.tsx +178 -0
  13. package/components/ui/input.tsx +1 -1
  14. package/components/ui/separator.tsx +1 -1
  15. package/components/ui/sheet.tsx +1 -1
  16. package/components/ui/sidebar.tsx +8 -8
  17. package/components/ui/skeleton.tsx +1 -1
  18. package/components/ui/tooltip.tsx +1 -1
  19. package/dist/activity/logger.js +21 -1
  20. package/dist/activity/logger.js.map +1 -1
  21. package/dist/activity/logger.mjs +4 -4
  22. package/dist/activity/queries.d.mts +1 -1
  23. package/dist/activity/queries.d.ts +1 -1
  24. package/dist/activity/queries.js +21 -1
  25. package/dist/activity/queries.js.map +1 -1
  26. package/dist/activity/queries.mjs +3 -3
  27. package/dist/admin/queries.js +21 -1
  28. package/dist/admin/queries.js.map +1 -1
  29. package/dist/admin/queries.mjs +3 -3
  30. package/dist/api/feedback.d.mts +23 -0
  31. package/dist/api/feedback.d.ts +23 -0
  32. package/dist/api/feedback.js +1224 -0
  33. package/dist/api/feedback.js.map +1 -0
  34. package/dist/api/feedback.mjs +136 -0
  35. package/dist/api/feedback.mjs.map +1 -0
  36. package/dist/auth/helpers.d.mts +3 -3
  37. package/dist/auth/helpers.d.ts +3 -3
  38. package/dist/auth/helpers.js +45 -4
  39. package/dist/auth/helpers.js.map +1 -1
  40. package/dist/auth/helpers.mjs +7 -7
  41. package/dist/auth/helpers.mjs.map +1 -1
  42. package/dist/auth/index.d.mts +8 -2
  43. package/dist/auth/index.d.ts +8 -2
  44. package/dist/auth/index.js +44 -3
  45. package/dist/auth/index.js.map +1 -1
  46. package/dist/auth/index.mjs +6 -6
  47. package/dist/auth-client/index.d.mts +16 -16
  48. package/dist/auth-client/index.d.ts +16 -16
  49. package/dist/{chunk-IF3JZRSZ.mjs → chunk-3MGEAGHU.mjs} +6 -5
  50. package/dist/chunk-3MGEAGHU.mjs.map +1 -0
  51. package/dist/chunk-KAEA35OC.mjs +76 -0
  52. package/dist/chunk-KAEA35OC.mjs.map +1 -0
  53. package/dist/{chunk-GTPTVNY2.mjs → chunk-TMJYAOC2.mjs} +10 -2
  54. package/dist/chunk-TMJYAOC2.mjs.map +1 -0
  55. package/dist/{chunk-OHAUKE2Q.mjs → chunk-VQ5B4MA7.mjs} +4 -4
  56. package/dist/{chunk-OHAUKE2Q.mjs.map → chunk-VQ5B4MA7.mjs.map} +1 -1
  57. package/dist/{chunk-5PYDAD4P.mjs → chunk-WZDAMCUX.mjs} +4 -4
  58. package/dist/{chunk-5PYDAD4P.mjs.map → chunk-WZDAMCUX.mjs.map} +1 -1
  59. package/dist/{chunk-V72CFFU2.mjs → chunk-XWVAJXAG.mjs} +31 -11
  60. package/dist/chunk-XWVAJXAG.mjs.map +1 -0
  61. package/dist/{chunk-4B7MJOLH.mjs → chunk-ZQCALTKB.mjs} +17 -5
  62. package/dist/chunk-ZQCALTKB.mjs.map +1 -0
  63. package/dist/config/env.d.mts +3 -0
  64. package/dist/config/env.d.ts +3 -0
  65. package/dist/config/env.js +8 -0
  66. package/dist/config/env.js.map +1 -1
  67. package/dist/config/env.mjs +1 -1
  68. package/dist/db/index.d.mts +1 -1
  69. package/dist/db/index.d.ts +1 -1
  70. package/dist/db/index.js +21 -1
  71. package/dist/db/index.js.map +1 -1
  72. package/dist/db/index.mjs +3 -3
  73. package/dist/db/schema.d.mts +1 -1
  74. package/dist/db/schema.d.ts +1 -1
  75. package/dist/db/schema.js +13 -1
  76. package/dist/db/schema.js.map +1 -1
  77. package/dist/db/schema.mjs +1 -1
  78. package/dist/errors.d.mts +29 -0
  79. package/dist/errors.d.ts +29 -0
  80. package/dist/errors.js +365 -0
  81. package/dist/errors.js.map +1 -0
  82. package/dist/errors.mjs +7 -0
  83. package/dist/errors.mjs.map +1 -0
  84. package/dist/{schema-BghJ8N4X.d.mts → schema-BNSBWNjK.d.mts} +99 -6
  85. package/dist/{schema-BghJ8N4X.d.ts → schema-BNSBWNjK.d.ts} +99 -6
  86. package/dist/sessions/queries.js +21 -1
  87. package/dist/sessions/queries.js.map +1 -1
  88. package/dist/sessions/queries.mjs +3 -3
  89. package/dist/settings/queries.d.mts +2 -2
  90. package/dist/settings/queries.d.ts +2 -2
  91. package/dist/settings/queries.js +22 -1
  92. package/dist/settings/queries.js.map +1 -1
  93. package/dist/settings/queries.mjs +4 -4
  94. package/dist/users/queries.d.mts +3 -3
  95. package/dist/users/queries.d.ts +3 -3
  96. package/dist/users/queries.js +21 -1
  97. package/dist/users/queries.js.map +1 -1
  98. package/dist/users/queries.mjs +3 -3
  99. package/hooks/use-mobile.ts +19 -0
  100. package/lib/device-fingerprint.ts +2 -3
  101. package/package.json +35 -32
  102. package/dist/chunk-4B7MJOLH.mjs.map +0 -1
  103. package/dist/chunk-GTPTVNY2.mjs.map +0 -1
  104. package/dist/chunk-IF3JZRSZ.mjs.map +0 -1
  105. package/dist/chunk-V72CFFU2.mjs.map +0 -1
@@ -47,6 +47,9 @@ declare function createAuth(overrides?: AuthOverrides): better_auth.Auth<{
47
47
  input: false;
48
48
  };
49
49
  };
50
+ deleteUser: {
51
+ enabled: true;
52
+ };
50
53
  };
51
54
  advanced: {
52
55
  database: {
@@ -888,7 +891,7 @@ declare function createAuth(overrides?: AuthOverrides): better_auth.Auth<{
888
891
  $Infer: {
889
892
  body: {
890
893
  permissions: {
891
- readonly user?: ("delete" | "update" | "list" | "set-role" | "create" | "ban" | "impersonate" | "impersonate-admins" | "set-password" | "set-email" | "get")[] | undefined;
894
+ readonly user?: ("set-role" | "delete" | "update" | "list" | "create" | "ban" | "impersonate" | "impersonate-admins" | "set-password" | "set-email" | "get")[] | undefined;
892
895
  readonly session?: ("delete" | "list" | "revoke")[] | undefined;
893
896
  };
894
897
  } & {
@@ -2328,6 +2331,9 @@ declare const auth: better_auth.Auth<{
2328
2331
  input: false;
2329
2332
  };
2330
2333
  };
2334
+ deleteUser: {
2335
+ enabled: true;
2336
+ };
2331
2337
  };
2332
2338
  advanced: {
2333
2339
  database: {
@@ -3169,7 +3175,7 @@ declare const auth: better_auth.Auth<{
3169
3175
  $Infer: {
3170
3176
  body: {
3171
3177
  permissions: {
3172
- readonly user?: ("delete" | "update" | "list" | "set-role" | "create" | "ban" | "impersonate" | "impersonate-admins" | "set-password" | "set-email" | "get")[] | undefined;
3178
+ readonly user?: ("set-role" | "delete" | "update" | "list" | "create" | "ban" | "impersonate" | "impersonate-admins" | "set-password" | "set-email" | "get")[] | undefined;
3173
3179
  readonly session?: ("delete" | "list" | "revoke")[] | undefined;
3174
3180
  };
3175
3181
  } & {
@@ -47,6 +47,9 @@ declare function createAuth(overrides?: AuthOverrides): better_auth.Auth<{
47
47
  input: false;
48
48
  };
49
49
  };
50
+ deleteUser: {
51
+ enabled: true;
52
+ };
50
53
  };
51
54
  advanced: {
52
55
  database: {
@@ -888,7 +891,7 @@ declare function createAuth(overrides?: AuthOverrides): better_auth.Auth<{
888
891
  $Infer: {
889
892
  body: {
890
893
  permissions: {
891
- readonly user?: ("delete" | "update" | "list" | "set-role" | "create" | "ban" | "impersonate" | "impersonate-admins" | "set-password" | "set-email" | "get")[] | undefined;
894
+ readonly user?: ("set-role" | "delete" | "update" | "list" | "create" | "ban" | "impersonate" | "impersonate-admins" | "set-password" | "set-email" | "get")[] | undefined;
892
895
  readonly session?: ("delete" | "list" | "revoke")[] | undefined;
893
896
  };
894
897
  } & {
@@ -2328,6 +2331,9 @@ declare const auth: better_auth.Auth<{
2328
2331
  input: false;
2329
2332
  };
2330
2333
  };
2334
+ deleteUser: {
2335
+ enabled: true;
2336
+ };
2331
2337
  };
2332
2338
  advanced: {
2333
2339
  database: {
@@ -3169,7 +3175,7 @@ declare const auth: better_auth.Auth<{
3169
3175
  $Infer: {
3170
3176
  body: {
3171
3177
  permissions: {
3172
- readonly user?: ("delete" | "update" | "list" | "set-role" | "create" | "ban" | "impersonate" | "impersonate-admins" | "set-password" | "set-email" | "get")[] | undefined;
3178
+ readonly user?: ("set-role" | "delete" | "update" | "list" | "create" | "ban" | "impersonate" | "impersonate-admins" | "set-password" | "set-email" | "get")[] | undefined;
3173
3179
  readonly session?: ("delete" | "list" | "revoke")[] | undefined;
3174
3180
  };
3175
3181
  } & {
@@ -208,6 +208,7 @@ __export(schema_exports, {
208
208
  activityLogs: () => activityLogs,
209
209
  authThrottle: () => authThrottle,
210
210
  deviceFingerprints: () => deviceFingerprints,
211
+ errorRefs: () => errorRefs,
211
212
  passkeys: () => passkeys,
212
213
  sessions: () => sessions,
213
214
  systemSettings: () => systemSettings,
@@ -227,7 +228,7 @@ var DEFAULT_MAINTENANCE_EXEMPT_PATHS = [
227
228
  ];
228
229
 
229
230
  // server/db/schema.ts
230
- var RoleEnum = pgCore.pgEnum("roles", ["user", "admin"]);
231
+ var RoleEnum = pgCore.pgEnum("roles", ["user", "admin", "tester"]);
231
232
  var ActivityActionEnum = pgCore.pgEnum("activity_actions", [
232
233
  "login",
233
234
  "logout",
@@ -390,6 +391,9 @@ var systemSettings = pgCore.pgTable("system_settings", {
390
391
  authEnableEmailPassword: pgCore.boolean("authEnableEmailPassword").notNull().default(true),
391
392
  authEnableOneTap: pgCore.boolean("authEnableOneTap").notNull().default(false),
392
393
  enableSessionRevocation: pgCore.boolean("enableSessionRevocation").notNull().default(true),
394
+ // Attach the error a user hit to feedback they send from it (see
395
+ // lib/feedback-route.ts). Off = only what the user typed is sent.
396
+ feedbackAttachErrors: pgCore.boolean("feedbackAttachErrors").notNull().default(true),
393
397
  updatedAt: pgCore.timestamp("updatedAt").notNull().defaultNow(),
394
398
  // Who last changed a setting. This row doesn't "belong" to that admin the
395
399
  // way sessions/activityLogs belong to their user, so deleting that admin's
@@ -398,15 +402,31 @@ var systemSettings = pgCore.pgTable("system_settings", {
398
402
  }, (table) => [
399
403
  pgCore.check("system_settings_singleton", drizzleOrm.sql`${table.id} = 'default'`)
400
404
  ]);
405
+ var errorRefs = pgCore.pgTable("error_refs", {
406
+ ref: pgCore.text("ref").primaryKey(),
407
+ message: pgCore.text("message").notNull(),
408
+ detail: pgCore.text("detail").notNull(),
409
+ createdAt: pgCore.timestamp("createdAt").notNull().defaultNow()
410
+ }, (table) => [
411
+ pgCore.index("error_refs_created_at_idx").on(table.createdAt)
412
+ ]);
401
413
  var envSchema = zod.z.object({
402
414
  NODE_ENV: zod.z.enum(["development", "production", "test"]).default("development"),
403
415
  PORT: zod.z.string().default("3000").transform(Number),
404
416
  DATABASE_URL: zod.z.string().default(""),
405
417
  RESEND_API_KEY: zod.z.string().default(""),
418
+ // Sender for every transactional email. Must be on a domain verified in
419
+ // your Resend account.
420
+ EMAIL_FROM: zod.z.string().default("Acme <onboarding@xavierkhew.xyz>"),
406
421
  // Display name for this deployment — shown as the authenticator-app label
407
422
  // (2FA issuer) and the passkey relying-party name. Consuming projects set
408
423
  // this in their own .env; defaults to the template name.
409
424
  APP_NAME: zod.z.string().default("Next.js Template"),
425
+ // Where the dashboard feedback form delivers (app/api/feedback). Set either
426
+ // or both; with neither, the Feedback button is hidden. FEEDBACK_EMAIL takes
427
+ // a comma-separated list and sends through Resend (needs RESEND_API_KEY).
428
+ FEEDBACK_DISCORD_WEBHOOK_URL: zod.z.string().default(""),
429
+ FEEDBACK_EMAIL: zod.z.string().default(""),
410
430
  // Seed-only: the live value lives in the system_settings DB row (see
411
431
  // lib/settings-queries.ts) once it's been seeded on first read, and is
412
432
  // changed from the admin System Settings page, not by editing this var.
@@ -592,6 +612,7 @@ function fallbackSettings() {
592
612
  authEnableEmailPassword: config.AUTH_ENABLE_EMAIL_PASSWORD,
593
613
  authEnableOneTap: config.AUTH_ENABLE_ONE_TAP,
594
614
  enableSessionRevocation: config.ENABLE_SESSION_REVOCATION,
615
+ feedbackAttachErrors: true,
595
616
  updatedAt: /* @__PURE__ */ new Date(0),
596
617
  updatedBy: null
597
618
  };
@@ -632,7 +653,7 @@ async function getEffectiveAuthFlags() {
632
653
  oneTap: config.AUTH_ENABLE_ONE_TAP && settings.authEnableOneTap
633
654
  };
634
655
  }
635
- var EMAIL_FROM = "Acme <onboarding@xavierkhew.xyz>";
656
+ var EMAIL_FROM = config.EMAIL_FROM;
636
657
  var resend;
637
658
  async function getResend() {
638
659
  if (!resend) {
@@ -784,7 +805,13 @@ function createAuth(overrides = {}) {
784
805
  input: false
785
806
  // Prevent users from setting their own role
786
807
  }
787
- }
808
+ },
809
+ // Self-service "Delete account" (dashboard Settings → Danger zone).
810
+ // Credential users confirm with their password; OAuth-only users have
811
+ // no password, so better-auth instead requires a session younger than
812
+ // `freshAge` (default 1 day). Every user-owned table cascades on delete.
813
+ // Impersonation and last-admin guards live in hooks.before below.
814
+ deleteUser: { enabled: true }
788
815
  },
789
816
  // Advanced options
790
817
  advanced: {
@@ -888,6 +915,20 @@ function createAuth(overrides = {}) {
888
915
  }
889
916
  return;
890
917
  }
918
+ if (ctx.path === "/delete-user") {
919
+ const current2 = await api.getSessionFromCtx(ctx, { disableCookieCache: true });
920
+ if (!current2) return;
921
+ if (current2.session.impersonatedBy) {
922
+ throw new api.APIError("FORBIDDEN", { message: "Stop impersonating before deleting an account." });
923
+ }
924
+ if (current2.user.role === "admin") {
925
+ const [{ count }] = await db.select({ count: drizzleOrm.sql`count(*)` }).from(users).where(drizzleOrm.eq(users.role, "admin"));
926
+ if (Number(count) <= 1) {
927
+ throw new api.APIError("FORBIDDEN", { message: "You're the only admin. Promote another user to admin first." });
928
+ }
929
+ }
930
+ return;
931
+ }
891
932
  if (ctx.path !== "/admin/stop-impersonating") return;
892
933
  const current = await api.getSessionFromCtx(ctx, { disableCookieCache: true });
893
934
  if (current == null ? void 0 : current.session.impersonatedBy) {