@xk2800/nextjs-template 0.8.0-beta.4 → 0.8.0-beta.6

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 (71) hide show
  1. package/components/dashboard/admin/feedbackTestForm.tsx +55 -0
  2. package/components/dashboard/admin/systemSettingsForm.tsx +28 -0
  3. package/components/dashboard/errorFeedback.tsx +65 -0
  4. package/components/dashboard/feedbackDialog.tsx +53 -11
  5. package/dist/activity/logger.js +12 -0
  6. package/dist/activity/logger.js.map +1 -1
  7. package/dist/activity/logger.mjs +3 -3
  8. package/dist/activity/queries.d.mts +1 -1
  9. package/dist/activity/queries.d.ts +1 -1
  10. package/dist/activity/queries.js +12 -0
  11. package/dist/activity/queries.js.map +1 -1
  12. package/dist/activity/queries.mjs +2 -2
  13. package/dist/admin/queries.js +12 -0
  14. package/dist/admin/queries.js.map +1 -1
  15. package/dist/admin/queries.mjs +2 -2
  16. package/dist/api/feedback.d.mts +15 -1
  17. package/dist/api/feedback.d.ts +15 -1
  18. package/dist/api/feedback.js +90 -3
  19. package/dist/api/feedback.js.map +1 -1
  20. package/dist/api/feedback.mjs +38 -9
  21. package/dist/api/feedback.mjs.map +1 -1
  22. package/dist/auth/helpers.js +13 -0
  23. package/dist/auth/helpers.js.map +1 -1
  24. package/dist/auth/helpers.mjs +5 -5
  25. package/dist/auth/index.js +13 -0
  26. package/dist/auth/index.js.map +1 -1
  27. package/dist/auth/index.mjs +5 -5
  28. package/dist/{chunk-IFZGEDMN.mjs → chunk-3MGEAGHU.mjs} +5 -4
  29. package/dist/chunk-3MGEAGHU.mjs.map +1 -0
  30. package/dist/chunk-KAEA35OC.mjs +76 -0
  31. package/dist/chunk-KAEA35OC.mjs.map +1 -0
  32. package/dist/{chunk-G3B7NC5B.mjs → chunk-VQ5B4MA7.mjs} +4 -4
  33. package/dist/{chunk-G3B7NC5B.mjs.map → chunk-VQ5B4MA7.mjs.map} +1 -1
  34. package/dist/{chunk-LKPF3JCV.mjs → chunk-WZDAMCUX.mjs} +3 -3
  35. package/dist/{chunk-LKPF3JCV.mjs.map → chunk-WZDAMCUX.mjs.map} +1 -1
  36. package/dist/{chunk-GS33BWOL.mjs → chunk-XWVAJXAG.mjs} +6 -6
  37. package/dist/{chunk-GS33BWOL.mjs.map → chunk-XWVAJXAG.mjs.map} +1 -1
  38. package/dist/{chunk-YPKOQOTA.mjs → chunk-ZQCALTKB.mjs} +16 -4
  39. package/dist/chunk-ZQCALTKB.mjs.map +1 -0
  40. package/dist/db/index.d.mts +1 -1
  41. package/dist/db/index.d.ts +1 -1
  42. package/dist/db/index.js +12 -0
  43. package/dist/db/index.js.map +1 -1
  44. package/dist/db/index.mjs +2 -2
  45. package/dist/db/schema.d.mts +1 -1
  46. package/dist/db/schema.d.ts +1 -1
  47. package/dist/db/schema.js +12 -0
  48. package/dist/db/schema.js.map +1 -1
  49. package/dist/db/schema.mjs +1 -1
  50. package/dist/errors.d.mts +29 -0
  51. package/dist/errors.d.ts +29 -0
  52. package/dist/errors.js +365 -0
  53. package/dist/errors.js.map +1 -0
  54. package/dist/errors.mjs +7 -0
  55. package/dist/errors.mjs.map +1 -0
  56. package/dist/{schema-CZnQXmhF.d.mts → schema-BNSBWNjK.d.mts} +95 -2
  57. package/dist/{schema-CZnQXmhF.d.ts → schema-BNSBWNjK.d.ts} +95 -2
  58. package/dist/sessions/queries.js +12 -0
  59. package/dist/sessions/queries.js.map +1 -1
  60. package/dist/sessions/queries.mjs +2 -2
  61. package/dist/settings/queries.d.mts +2 -2
  62. package/dist/settings/queries.d.ts +2 -2
  63. package/dist/settings/queries.js +13 -0
  64. package/dist/settings/queries.js.map +1 -1
  65. package/dist/settings/queries.mjs +3 -3
  66. package/dist/users/queries.js +12 -0
  67. package/dist/users/queries.js.map +1 -1
  68. package/dist/users/queries.mjs +2 -2
  69. package/package.json +9 -1
  70. package/dist/chunk-IFZGEDMN.mjs.map +0 -1
  71. package/dist/chunk-YPKOQOTA.mjs.map +0 -1
@@ -1,9 +1,10 @@
1
- import { auth, getResend, EMAIL_FROM } from '../chunk-GS33BWOL.mjs';
2
- import '../chunk-G3B7NC5B.mjs';
1
+ import { lookupErrorDetail, unexpectedErrorMessage } from '../chunk-KAEA35OC.mjs';
2
+ import { auth, getResend, EMAIL_FROM } from '../chunk-XWVAJXAG.mjs';
3
+ import '../chunk-VQ5B4MA7.mjs';
3
4
  import '../chunk-N6ECRCHY.mjs';
4
- import '../chunk-IFZGEDMN.mjs';
5
- import '../chunk-LKPF3JCV.mjs';
6
- import '../chunk-YPKOQOTA.mjs';
5
+ import { getSystemSettings } from '../chunk-3MGEAGHU.mjs';
6
+ import '../chunk-WZDAMCUX.mjs';
7
+ import '../chunk-ZQCALTKB.mjs';
7
8
  import { config } from '../chunk-TMJYAOC2.mjs';
8
9
  import '../chunk-3465SI5N.mjs';
9
10
  import 'server-only';
@@ -15,9 +16,11 @@ var feedbackSchema = z.object({
15
16
  type: z.enum(["bug", "feedback"]),
16
17
  // Keep in sync with FEEDBACK_MAX_LENGTH in components/dashboard/feedbackDialog.tsx.
17
18
  message: z.string().trim().min(1).max(2e3),
18
- page: z.string().max(300).optional()
19
+ page: z.string().max(300).optional(),
20
+ // The error the user reported from (FeedbackDialog's errorDetails).
21
+ error: z.string().max(2e3).optional()
19
22
  });
20
- var COOLDOWN_MS = 3e4;
23
+ var COOLDOWN_MS = 0;
21
24
  var lastSentAt = /* @__PURE__ */ new Map();
22
25
  async function POST(request) {
23
26
  var _a;
@@ -43,6 +46,8 @@ async function POST(request) {
43
46
  }
44
47
  lastSentAt.set(session.user.id, now);
45
48
  const { type, message, page } = parsed.data;
49
+ const errorDetails = parsed.data.error && (await getSystemSettings()).feedbackAttachErrors ? parsed.data.error : void 0;
50
+ const technicalDetail = errorDetails ? await lookupErrorDetail(errorDetails) : null;
46
51
  const { user } = session;
47
52
  const title = type === "bug" ? "\u{1F41E} Bug report" : "\u{1F4AC} Feedback";
48
53
  const userAgent = request.headers.get("user-agent") || "unknown";
@@ -63,6 +68,8 @@ async function POST(request) {
63
68
  fields: [
64
69
  { name: "User", value: `${user.name} (${user.email})`.slice(0, 1024) },
65
70
  { name: "Page", value: page || "unknown", inline: true },
71
+ ...errorDetails ? [{ name: "Error", value: errorDetails.slice(0, 1024) }] : [],
72
+ ...technicalDetail ? [{ name: "Technical details", value: "```\n" + technicalDetail.slice(0, 1e3) + "\n```" }] : [],
66
73
  { name: "User agent", value: userAgent.slice(0, 1024) }
67
74
  ],
68
75
  timestamp: new Date(now).toISOString()
@@ -84,7 +91,11 @@ async function POST(request) {
84
91
 
85
92
  ---
86
93
  User: ${user.name} <${user.email}>
87
- Page: ${page || "unknown"}
94
+ Page: ${page || "unknown"}${errorDetails ? `
95
+ Error: ${errorDetails}` : ""}${technicalDetail ? `
96
+
97
+ Technical details:
98
+ ${technicalDetail}` : ""}
88
99
  User agent: ${userAgent}`
89
100
  })).then(({ error }) => {
90
101
  if (error) throw new Error(error.message);
@@ -101,7 +112,25 @@ User agent: ${userAgent}`
101
112
  }
102
113
  return NextResponse.json({ success: true });
103
114
  }
115
+ async function testErrorPOST(request) {
116
+ if (process.env.NODE_ENV !== "development") {
117
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
118
+ }
119
+ const session = await auth.api.getSession({ headers: await headers() });
120
+ if (!(session == null ? void 0 : session.user)) {
121
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
122
+ }
123
+ const body = await request.json().catch(() => null);
124
+ if (typeof (body == null ? void 0 : body.text) === "string" && body.text.trim() === "!error") {
125
+ return NextResponse.json({ error: "Test error from the feedback test form" }, { status: 400 });
126
+ }
127
+ if (typeof (body == null ? void 0 : body.text) === "string" && body.text.trim() === "!crash") {
128
+ const crash = new Error("Test crash: DB connection to postgres://admin:hunter2@db.internal failed");
129
+ return NextResponse.json({ error: unexpectedErrorMessage(crash) }, { status: 500 });
130
+ }
131
+ return NextResponse.json({ success: true });
132
+ }
104
133
 
105
- export { POST };
134
+ export { POST, testErrorPOST };
106
135
  //# sourceMappingURL=feedback.mjs.map
107
136
  //# sourceMappingURL=feedback.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/feedback-route.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAWA,IAAM,cAAA,GAAiB,EAAE,MAAA,CAAO;AAAA,EAC9B,MAAM,CAAA,CAAE,IAAA,CAAK,CAAC,KAAA,EAAO,UAAU,CAAC,CAAA;AAAA;AAAA,EAEhC,OAAA,EAAS,CAAA,CAAE,MAAA,EAAO,CAAE,IAAA,GAAO,GAAA,CAAI,CAAC,CAAA,CAAE,GAAA,CAAI,GAAI,CAAA;AAAA,EAC1C,MAAM,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,GAAG,EAAE,QAAA;AAC5B,CAAC,CAAA;AAED,IAAM,WAAA,GAAc,GAAA;AAIpB,IAAM,UAAA,uBAAiB,GAAA,EAAoB;AAE3C,eAAsB,KAAK,OAAA,EAAsB;AAxBjD,EAAA,IAAA,EAAA;AAyBE,EAAA,MAAM,aAAa,MAAA,CAAO,4BAAA;AAC1B,EAAA,MAAM,UAAU,MAAA,CAAO,cAAA;AACvB,EAAA,IAAI,CAAC,UAAA,IAAc,CAAC,OAAA,EAAS;AAC3B,IAAA,OAAO,YAAA,CAAa,KAAK,EAAE,KAAA,EAAO,8BAA6B,EAAG,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAA;AAAA,EACnF;AAEA,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,GAAA,CAAI,UAAA,CAAW,EAAE,OAAA,EAAS,MAAM,OAAA,EAAQ,EAAG,CAAA;AACtE,EAAA,IAAI,EAAC,mCAAS,IAAA,CAAA,EAAM;AAClB,IAAA,OAAO,YAAA,CAAa,KAAK,EAAE,KAAA,EAAO,gBAAe,EAAG,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAA;AAAA,EACrE;AAEA,EAAA,MAAM,MAAA,GAAS,cAAA,CAAe,SAAA,CAAU,MAAM,OAAA,CAAQ,MAAK,CAAE,KAAA,CAAM,MAAM,IAAI,CAAC,CAAA;AAC9E,EAAA,IAAI,CAAC,OAAO,OAAA,EAAS;AACnB,IAAA,OAAO,YAAA,CAAa,KAAK,EAAE,KAAA,EAAO,qCAAoC,EAAG,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAA;AAAA,EAC1F;AAEA,EAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,EAAA,IAAI,GAAA,IAAA,CAAO,gBAAW,GAAA,CAAI,OAAA,CAAQ,KAAK,EAAE,CAAA,KAA9B,IAAA,GAAA,EAAA,GAAmC,CAAA,CAAA,GAAK,WAAA,EAAa;AAC9D,IAAA,OAAO,YAAA,CAAa,IAAA;AAAA,MAClB,EAAE,OAAO,mDAAA,EAAoD;AAAA,MAC7D,EAAE,QAAQ,GAAA;AAAI,KAChB;AAAA,EACF;AACA,EAAA,UAAA,CAAW,GAAA,CAAI,OAAA,CAAQ,IAAA,CAAK,EAAA,EAAI,GAAG,CAAA;AAEnC,EAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAS,IAAA,KAAS,MAAA,CAAO,IAAA;AACvC,EAAA,MAAM,EAAE,MAAK,GAAI,OAAA;AACjB,EAAA,MAAM,KAAA,GAAQ,IAAA,KAAS,KAAA,GAAQ,sBAAA,GAAkB,oBAAA;AACjD,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAA,IAAK,SAAA;AAEvD,EAAA,MAAM,QAA4B,EAAC;AAEnC,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,MAAM,UAAA,EAAY;AAAA,QAChB,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,UACnB,UAAU,MAAA,CAAO,QAAA;AAAA;AAAA,UAEjB,gBAAA,EAAkB,EAAE,KAAA,EAAO,EAAC,EAAE;AAAA,UAC9B,QAAQ,CAAC;AAAA,YACP,KAAA;AAAA,YACA,WAAA,EAAa,OAAA;AAAA,YACb,KAAA,EAAO,IAAA,KAAS,KAAA,GAAQ,QAAA,GAAW,OAAA;AAAA,YACnC,MAAA,EAAQ;AAAA,cACN,EAAE,IAAA,EAAM,MAAA,EAAQ,KAAA,EAAO,GAAG,IAAA,CAAK,IAAI,CAAA,EAAA,EAAK,IAAA,CAAK,KAAK,CAAA,CAAA,CAAA,CAAI,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA,EAAE;AAAA,cACrE,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAO,IAAA,IAAQ,SAAA,EAAW,QAAQ,IAAA,EAAK;AAAA,cACvD,EAAE,MAAM,YAAA,EAAc,KAAA,EAAO,UAAU,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA;AAAE,aACxD;AAAA,YACA,SAAA,EAAW,IAAI,IAAA,CAAK,GAAG,EAAE,WAAA;AAAY,WACtC;AAAA,SACF;AAAA,OACF,CAAA,CAAE,IAAA,CAAK,CAAC,GAAA,KAAQ;AACf,QAAA,IAAI,CAAC,IAAI,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAAA,MACxE,CAAC;AAAA,KACH;AAAA,EACF;AAEA,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,WAAU,CACP,IAAA,CAAK,CAAC,MAAA,KAAW,MAAA,CAAO,OAAO,IAAA,CAAK;AAAA,QACnC,IAAA,EAAM,UAAA;AAAA,QACN,EAAA,EAAI,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA,CAAE,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAM,CAAA;AAAA,QAC1C,SAAS,IAAA,CAAK,KAAA;AAAA,QACd,OAAA,EAAS,IAAI,MAAA,CAAO,QAAQ,KAAK,KAAK,CAAA,MAAA,EAAS,KAAK,IAAI,CAAA,CAAA;AAAA,QACxD,IAAA,EAAM,GAAG,OAAO;;AAAA;AAAA,MAAA,EAAkB,IAAA,CAAK,IAAI,CAAA,EAAA,EAAK,IAAA,CAAK,KAAK,CAAA;AAAA,MAAA,EAAY,QAAQ,SAAS;AAAA,YAAA,EAAiB,SAAS,CAAA;AAAA,OAClH,CAAC,CAAA,CACD,KAAK,CAAC,EAAE,OAAM,KAAM;AACnB,QAAA,IAAI,KAAA,EAAO,MAAM,IAAI,KAAA,CAAM,MAAM,OAAO,CAAA;AAAA,MAC1C,CAAC;AAAA,KACL;AAAA,EACF;AAEA,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,UAAA,CAAW,KAAK,CAAA;AAC9C,EAAA,OAAA,CAAQ,OAAA,CAAQ,CAAC,CAAA,KAAM;AACrB,IAAA,IAAI,EAAE,MAAA,KAAW,UAAA,UAAoB,KAAA,CAAM,2BAAA,EAA6B,EAAE,MAAM,CAAA;AAAA,EAClF,CAAC,CAAA;AAED,EAAA,IAAI,CAAC,QAAQ,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,MAAA,KAAW,WAAW,CAAA,EAAG;AAClD,IAAA,UAAA,CAAW,MAAA,CAAO,KAAK,EAAE,CAAA;AACzB,IAAA,OAAO,YAAA,CAAa,KAAK,EAAE,KAAA,EAAO,2BAA0B,EAAG,EAAE,MAAA,EAAQ,GAAA,EAAK,CAAA;AAAA,EAChF;AACA,EAAA,OAAO,YAAA,CAAa,IAAA,CAAK,EAAE,OAAA,EAAS,MAAM,CAAA;AAC5C","file":"feedback.mjs","sourcesContent":["import \"server-only\"\n// POST handler behind components/dashboard/feedbackDialog.tsx. Shipped as\n// `@xk2800/nextjs-template/api/feedback` so apps mount it with a one-line\n// app/api/feedback/route.ts: export { POST } from \"@xk2800/nextjs-template/api/feedback\"\nimport { NextRequest, NextResponse } from \"next/server\"\nimport { headers } from \"next/headers\"\nimport { z } from \"zod\"\nimport { auth } from \"../server/auth\"\nimport { config } from \"../config/env\"\nimport { getResend, EMAIL_FROM } from \"./resend\"\n\nconst feedbackSchema = z.object({\n type: z.enum([\"bug\", \"feedback\"]),\n // Keep in sync with FEEDBACK_MAX_LENGTH in components/dashboard/feedbackDialog.tsx.\n message: z.string().trim().min(1).max(2000),\n page: z.string().max(300).optional(),\n})\n\nconst COOLDOWN_MS = 30_000\n// const COOLDOWN_MS = 0\n// ponytail: in-memory, per instance; move to a DB/Redis counter if one user\n// spamming across instances ever matters.\nconst lastSentAt = new Map<string, number>()\n\nexport async function POST(request: NextRequest) {\n const discordUrl = config.FEEDBACK_DISCORD_WEBHOOK_URL\n const emailTo = config.FEEDBACK_EMAIL\n if (!discordUrl && !emailTo) {\n return NextResponse.json({ error: \"Feedback is not configured\" }, { status: 503 })\n }\n\n const session = await auth.api.getSession({ headers: await headers() })\n if (!session?.user) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n }\n\n const parsed = feedbackSchema.safeParse(await request.json().catch(() => null))\n if (!parsed.success) {\n return NextResponse.json({ error: \"Message must be 1-2000 characters\" }, { status: 400 })\n }\n\n const now = Date.now()\n if (now - (lastSentAt.get(session.user.id) ?? 0) < COOLDOWN_MS) {\n return NextResponse.json(\n { error: \"Please wait a moment before sending more feedback\" },\n { status: 429 }\n )\n }\n lastSentAt.set(session.user.id, now)\n\n const { type, message, page } = parsed.data\n const { user } = session\n const title = type === \"bug\" ? \"🐞 Bug report\" : \"💬 Feedback\"\n const userAgent = request.headers.get(\"user-agent\") || \"unknown\"\n\n const sends: Promise<unknown>[] = []\n\n if (discordUrl) {\n sends.push(\n fetch(discordUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n username: config.APP_NAME,\n // User text must never ping @everyone/@here/roles in the channel.\n allowed_mentions: { parse: [] },\n embeds: [{\n title,\n description: message,\n color: type === \"bug\" ? 0xef4444 : 0x3b82f6,\n fields: [\n { name: \"User\", value: `${user.name} (${user.email})`.slice(0, 1024) },\n { name: \"Page\", value: page || \"unknown\", inline: true },\n { name: \"User agent\", value: userAgent.slice(0, 1024) },\n ],\n timestamp: new Date(now).toISOString(),\n }],\n }),\n }).then((res) => {\n if (!res.ok) throw new Error(`Discord webhook responded ${res.status}`)\n })\n )\n }\n\n if (emailTo) {\n sends.push(\n getResend()\n .then((resend) => resend.emails.send({\n from: EMAIL_FROM,\n to: emailTo.split(\",\").map((s) => s.trim()),\n replyTo: user.email,\n subject: `[${config.APP_NAME}] ${title} from ${user.name}`,\n text: `${message}\\n\\n---\\nUser: ${user.name} <${user.email}>\\nPage: ${page || \"unknown\"}\\nUser agent: ${userAgent}`,\n }))\n .then(({ error }) => {\n if (error) throw new Error(error.message)\n })\n )\n }\n\n const results = await Promise.allSettled(sends)\n results.forEach((r) => {\n if (r.status === \"rejected\") console.error(\"Feedback delivery failed:\", r.reason)\n })\n\n if (!results.some((r) => r.status === \"fulfilled\")) {\n lastSentAt.delete(user.id)\n return NextResponse.json({ error: \"Failed to send feedback\" }, { status: 502 })\n }\n return NextResponse.json({ success: true })\n}\n"]}
1
+ {"version":3,"sources":["../../lib/feedback-route.ts"],"names":[],"mappings":"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","file":"feedback.mjs","sourcesContent":["import \"server-only\"\n// POST handler behind components/dashboard/feedbackDialog.tsx. Shipped as\n// `@xk2800/nextjs-template/api/feedback` so apps mount it with a one-line\n// app/api/feedback/route.ts: export { POST } from \"@xk2800/nextjs-template/api/feedback\"\nimport { NextRequest, NextResponse } from \"next/server\"\nimport { headers } from \"next/headers\"\nimport { z } from \"zod\"\nimport { auth } from \"../server/auth\"\nimport { config } from \"../config/env\"\nimport { getResend, EMAIL_FROM } from \"./resend\"\nimport { getSystemSettings } from \"./settings-queries\"\nimport { lookupErrorDetail, unexpectedErrorMessage } from \"./errors\"\n\nconst feedbackSchema = z.object({\n type: z.enum([\"bug\", \"feedback\"]),\n // Keep in sync with FEEDBACK_MAX_LENGTH in components/dashboard/feedbackDialog.tsx.\n message: z.string().trim().min(1).max(2000),\n page: z.string().max(300).optional(),\n // The error the user reported from (FeedbackDialog's errorDetails).\n error: z.string().max(2000).optional(),\n})\n\n// const COOLDOWN_MS = 30_000\nconst COOLDOWN_MS = 0\n// ponytail: in-memory, per instance; move to a DB/Redis counter if one user\n// spamming across instances ever matters.\nconst lastSentAt = new Map<string, number>()\n\nexport async function POST(request: NextRequest) {\n const discordUrl = config.FEEDBACK_DISCORD_WEBHOOK_URL\n const emailTo = config.FEEDBACK_EMAIL\n if (!discordUrl && !emailTo) {\n return NextResponse.json({ error: \"Feedback is not configured\" }, { status: 503 })\n }\n\n const session = await auth.api.getSession({ headers: await headers() })\n if (!session?.user) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n }\n\n const parsed = feedbackSchema.safeParse(await request.json().catch(() => null))\n if (!parsed.success) {\n return NextResponse.json({ error: \"Message must be 1-2000 characters\" }, { status: 400 })\n }\n\n const now = Date.now()\n if (now - (lastSentAt.get(session.user.id) ?? 0) < COOLDOWN_MS) {\n return NextResponse.json(\n { error: \"Please wait a moment before sending more feedback\" },\n { status: 429 }\n )\n }\n lastSentAt.set(session.user.id, now)\n\n const { type, message, page } = parsed.data\n // Admin-controlled (System Settings → Feedback); off drops it entirely.\n const errorDetails =\n parsed.data.error && (await getSystemSettings()).feedbackAttachErrors\n ? parsed.data.error\n : undefined\n // Technical detail stored under the error's ref (lib/errors.ts). Goes\n // server → Discord/email only; the browser never sees it.\n const technicalDetail = errorDetails ? await lookupErrorDetail(errorDetails) : null\n const { user } = session\n const title = type === \"bug\" ? \"🐞 Bug report\" : \"💬 Feedback\"\n const userAgent = request.headers.get(\"user-agent\") || \"unknown\"\n\n const sends: Promise<unknown>[] = []\n\n if (discordUrl) {\n sends.push(\n fetch(discordUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n username: config.APP_NAME,\n // User text must never ping @everyone/@here/roles in the channel.\n allowed_mentions: { parse: [] },\n embeds: [{\n title,\n description: message,\n color: type === \"bug\" ? 0xef4444 : 0x3b82f6,\n fields: [\n { name: \"User\", value: `${user.name} (${user.email})`.slice(0, 1024) },\n { name: \"Page\", value: page || \"unknown\", inline: true },\n ...(errorDetails ? [{ name: \"Error\", value: errorDetails.slice(0, 1024) }] : []),\n ...(technicalDetail\n ? [{ name: \"Technical details\", value: \"```\\n\" + technicalDetail.slice(0, 1000) + \"\\n```\" }]\n : []),\n { name: \"User agent\", value: userAgent.slice(0, 1024) },\n ],\n timestamp: new Date(now).toISOString(),\n }],\n }),\n }).then((res) => {\n if (!res.ok) throw new Error(`Discord webhook responded ${res.status}`)\n })\n )\n }\n\n if (emailTo) {\n sends.push(\n getResend()\n .then((resend) => resend.emails.send({\n from: EMAIL_FROM,\n to: emailTo.split(\",\").map((s) => s.trim()),\n replyTo: user.email,\n subject: `[${config.APP_NAME}] ${title} from ${user.name}`,\n text: `${message}\\n\\n---\\nUser: ${user.name} <${user.email}>\\nPage: ${page || \"unknown\"}${errorDetails ? `\\nError: ${errorDetails}` : \"\"}${technicalDetail ? `\\n\\nTechnical details:\\n${technicalDetail}` : \"\"}\\nUser agent: ${userAgent}`,\n }))\n .then(({ error }) => {\n if (error) throw new Error(error.message)\n })\n )\n }\n\n const results = await Promise.allSettled(sends)\n results.forEach((r) => {\n if (r.status === \"rejected\") console.error(\"Feedback delivery failed:\", r.reason)\n })\n\n if (!results.some((r) => r.status === \"fulfilled\")) {\n lastSentAt.delete(user.id)\n return NextResponse.json({ error: \"Failed to send feedback\" }, { status: 502 })\n }\n return NextResponse.json({ success: true })\n}\n\n/**\n * Development-only endpoint behind FeedbackTestForm, for testing the\n * report-from-error flow. \"!error\" answers with a safe, user-facing error;\n * \"!crash\" simulates an unexpected failure, so the user gets a generic message\n * with an error ref and the technical detail only reaches the server log.\n * 404 outside development.\n * Mount with app/api/feedback/test/route.ts:\n * export { testErrorPOST as POST } from \"@xk2800/nextjs-template/api/feedback\"\n */\nexport async function testErrorPOST(request: NextRequest) {\n if (process.env.NODE_ENV !== \"development\") {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 })\n }\n const session = await auth.api.getSession({ headers: await headers() })\n if (!session?.user) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n }\n const body = await request.json().catch(() => null)\n if (typeof body?.text === \"string\" && body.text.trim() === \"!error\") {\n return NextResponse.json({ error: \"Test error from the feedback test form\" }, { status: 400 })\n }\n if (typeof body?.text === \"string\" && body.text.trim() === \"!crash\") {\n const crash = new Error(\"Test crash: DB connection to postgres://admin:hunter2@db.internal failed\")\n return NextResponse.json({ error: unexpectedErrorMessage(crash) }, { status: 500 })\n }\n return NextResponse.json({ success: true })\n}\n"]}
@@ -211,6 +211,7 @@ __export(schema_exports, {
211
211
  activityLogs: () => activityLogs,
212
212
  authThrottle: () => authThrottle,
213
213
  deviceFingerprints: () => deviceFingerprints,
214
+ errorRefs: () => errorRefs,
214
215
  passkeys: () => passkeys,
215
216
  sessions: () => sessions,
216
217
  systemSettings: () => systemSettings,
@@ -393,6 +394,9 @@ var systemSettings = pgCore.pgTable("system_settings", {
393
394
  authEnableEmailPassword: pgCore.boolean("authEnableEmailPassword").notNull().default(true),
394
395
  authEnableOneTap: pgCore.boolean("authEnableOneTap").notNull().default(false),
395
396
  enableSessionRevocation: pgCore.boolean("enableSessionRevocation").notNull().default(true),
397
+ // Attach the error a user hit to feedback they send from it (see
398
+ // lib/feedback-route.ts). Off = only what the user typed is sent.
399
+ feedbackAttachErrors: pgCore.boolean("feedbackAttachErrors").notNull().default(true),
396
400
  updatedAt: pgCore.timestamp("updatedAt").notNull().defaultNow(),
397
401
  // Who last changed a setting. This row doesn't "belong" to that admin the
398
402
  // way sessions/activityLogs belong to their user, so deleting that admin's
@@ -401,6 +405,14 @@ var systemSettings = pgCore.pgTable("system_settings", {
401
405
  }, (table) => [
402
406
  pgCore.check("system_settings_singleton", drizzleOrm.sql`${table.id} = 'default'`)
403
407
  ]);
408
+ var errorRefs = pgCore.pgTable("error_refs", {
409
+ ref: pgCore.text("ref").primaryKey(),
410
+ message: pgCore.text("message").notNull(),
411
+ detail: pgCore.text("detail").notNull(),
412
+ createdAt: pgCore.timestamp("createdAt").notNull().defaultNow()
413
+ }, (table) => [
414
+ pgCore.index("error_refs_created_at_idx").on(table.createdAt)
415
+ ]);
404
416
  var envSchema = zod.z.object({
405
417
  NODE_ENV: zod.z.enum(["development", "production", "test"]).default("development"),
406
418
  PORT: zod.z.string().default("3000").transform(Number),
@@ -603,6 +615,7 @@ function fallbackSettings() {
603
615
  authEnableEmailPassword: config.AUTH_ENABLE_EMAIL_PASSWORD,
604
616
  authEnableOneTap: config.AUTH_ENABLE_ONE_TAP,
605
617
  enableSessionRevocation: config.ENABLE_SESSION_REVOCATION,
618
+ feedbackAttachErrors: true,
606
619
  updatedAt: /* @__PURE__ */ new Date(0),
607
620
  updatedBy: null
608
621
  };