@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.
- package/README.md +32 -30
- package/components/auth/deviceCheck.tsx +47 -6
- package/components/auth/oneTap.tsx +5 -5
- package/components/dashboard/admin/adminUserConsentCard.tsx +79 -0
- package/components/dashboard/admin/adminUserDevicesCard.tsx +80 -0
- package/components/dashboard/admin/broadcastActions.tsx +148 -0
- package/components/dashboard/admin/confirmSendDialog.tsx +44 -0
- package/components/dashboard/admin/marketingEmailForm.tsx +115 -0
- package/components/dashboard/admin/marketingSection.tsx +67 -0
- package/components/dashboard/admin/subscribersCard.tsx +62 -0
- package/components/dashboard/admin/systemSettingsForm.tsx +33 -0
- package/components/dashboard/admin/systemSettingsSection.tsx +8 -2
- package/components/dashboard/errorFeedback.tsx +2 -4
- package/components/dashboard/feedbackDialog.tsx +4 -4
- package/components/dashboard/marketingUpdatesCard.tsx +62 -0
- package/components/dashboard/marketingUpdatesSection.tsx +10 -0
- package/components/site/cookie-banner.tsx +109 -7
- package/components/ui/alert-dialog.tsx +1 -1
- package/components/ui/badge.tsx +1 -1
- package/components/ui/card.tsx +1 -1
- package/components/ui/data-table.tsx +4 -1
- package/components/ui/dialog.tsx +1 -1
- package/components/ui/dropdown-menu.tsx +2 -2
- package/components/ui/file-upload.tsx +129 -7
- package/components/ui/select.tsx +189 -0
- package/components/ui/switch.tsx +2 -2
- package/dist/activity/logger.js +36 -0
- package/dist/activity/logger.js.map +1 -1
- package/dist/activity/logger.mjs +4 -4
- package/dist/activity/queries.d.mts +1 -1
- package/dist/activity/queries.d.ts +1 -1
- package/dist/activity/queries.js +36 -0
- package/dist/activity/queries.js.map +1 -1
- package/dist/activity/queries.mjs +3 -3
- package/dist/admin/queries.d.mts +19 -1
- package/dist/admin/queries.d.ts +19 -1
- package/dist/admin/queries.js +64 -0
- package/dist/admin/queries.js.map +1 -1
- package/dist/admin/queries.mjs +26 -5
- package/dist/admin/queries.mjs.map +1 -1
- package/dist/api/feedback.js +53 -2
- package/dist/api/feedback.js.map +1 -1
- package/dist/api/feedback.mjs +9 -8
- package/dist/api/feedback.mjs.map +1 -1
- package/dist/api/marketing.d.mts +36 -0
- package/dist/api/marketing.d.ts +36 -0
- package/dist/api/marketing.js +1357 -0
- package/dist/api/marketing.js.map +1 -0
- package/dist/api/marketing.mjs +170 -0
- package/dist/api/marketing.mjs.map +1 -0
- package/dist/auth/helpers.js +51 -0
- package/dist/auth/helpers.js.map +1 -1
- package/dist/auth/helpers.mjs +9 -64
- package/dist/auth/helpers.mjs.map +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +51 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/index.mjs +8 -7
- package/dist/{chunk-KAEA35OC.mjs → chunk-6MDQRT6U.mjs} +4 -4
- package/dist/{chunk-KAEA35OC.mjs.map → chunk-6MDQRT6U.mjs.map} +1 -1
- package/dist/{chunk-N6ECRCHY.mjs → chunk-6OAHCEDJ.mjs} +4 -3
- package/dist/chunk-6OAHCEDJ.mjs.map +1 -0
- package/dist/chunk-6Z3WUSDJ.mjs +62 -0
- package/dist/chunk-6Z3WUSDJ.mjs.map +1 -0
- package/dist/{chunk-VQ5B4MA7.mjs → chunk-7F2POZM6.mjs} +4 -4
- package/dist/{chunk-VQ5B4MA7.mjs.map → chunk-7F2POZM6.mjs.map} +1 -1
- package/dist/{chunk-XWVAJXAG.mjs → chunk-7QVIOQ47.mjs} +23 -19
- package/dist/chunk-7QVIOQ47.mjs.map +1 -0
- package/dist/chunk-C6RGN6QA.mjs +60 -0
- package/dist/chunk-C6RGN6QA.mjs.map +1 -0
- package/dist/chunk-OVKVD2HH.mjs +25 -0
- package/dist/chunk-OVKVD2HH.mjs.map +1 -0
- package/dist/{chunk-3MGEAGHU.mjs → chunk-P2G23JMY.mjs} +11 -6
- package/dist/chunk-P2G23JMY.mjs.map +1 -0
- package/dist/{chunk-TMJYAOC2.mjs → chunk-TKWNEKK6.mjs} +8 -2
- package/dist/chunk-TKWNEKK6.mjs.map +1 -0
- package/dist/{chunk-ZQCALTKB.mjs → chunk-TR5XFBTD.mjs} +34 -4
- package/dist/chunk-TR5XFBTD.mjs.map +1 -0
- package/dist/{chunk-WZDAMCUX.mjs → chunk-ZJMFE5B2.mjs} +4 -4
- package/dist/{chunk-WZDAMCUX.mjs.map → chunk-ZJMFE5B2.mjs.map} +1 -1
- package/dist/config/env.d.mts +2 -0
- package/dist/config/env.d.ts +2 -0
- package/dist/config/env.js +6 -0
- package/dist/config/env.js.map +1 -1
- package/dist/config/env.mjs +1 -1
- package/dist/db/index.d.mts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +36 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/index.mjs +3 -3
- package/dist/db/schema.d.mts +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.js +30 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/db/schema.mjs +1 -1
- package/dist/errors.js +36 -0
- package/dist/errors.js.map +1 -1
- package/dist/errors.mjs +4 -4
- package/dist/marketing/index.d.mts +35 -0
- package/dist/marketing/index.d.ts +35 -0
- package/dist/marketing/index.js +125 -0
- package/dist/marketing/index.js.map +1 -0
- package/dist/marketing/index.mjs +6 -0
- package/dist/marketing/index.mjs.map +1 -0
- package/dist/{schema-BNSBWNjK.d.mts → schema-CN_Y6EXv.d.mts} +149 -3
- package/dist/{schema-BNSBWNjK.d.ts → schema-CN_Y6EXv.d.ts} +149 -3
- package/dist/sessions/queries.js +36 -0
- package/dist/sessions/queries.js.map +1 -1
- package/dist/sessions/queries.mjs +3 -3
- package/dist/settings/queries.d.mts +4 -3
- package/dist/settings/queries.d.ts +4 -3
- package/dist/settings/queries.js +42 -0
- package/dist/settings/queries.js.map +1 -1
- package/dist/settings/queries.mjs +4 -4
- package/dist/users/queries.js +36 -0
- package/dist/users/queries.js.map +1 -1
- package/dist/users/queries.mjs +3 -3
- package/lib/cookie-consent.ts +72 -20
- package/lib/cookie-names.ts +47 -0
- package/lib/device-fingerprint.ts +66 -10
- package/lib/recalculate-columns.ts +2 -0
- package/package.json +28 -3
- package/styles/theme.css +17 -3
- package/dist/chunk-3MGEAGHU.mjs.map +0 -1
- package/dist/chunk-N6ECRCHY.mjs.map +0 -1
- package/dist/chunk-TMJYAOC2.mjs.map +0 -1
- package/dist/chunk-XWVAJXAG.mjs.map +0 -1
- package/dist/chunk-ZQCALTKB.mjs.map +0 -1
package/dist/api/feedback.js
CHANGED
|
@@ -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
|
|
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() -
|
|
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
|
}
|