@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.
- 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 +3 -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 +120 -2
- package/components/ui/select.tsx +189 -0
- package/components/ui/switch.tsx +3 -3
- 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.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-UuqxoOzS.d.mts → schema-CN_Y6EXv.d.mts} +148 -2
- package/dist/{schema-UuqxoOzS.d.ts → schema-CN_Y6EXv.d.ts} +148 -2
- 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 +23 -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/auth/helpers.mjs
CHANGED
|
@@ -1,67 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
import '../chunk-
|
|
3
|
-
import '../chunk-
|
|
4
|
-
import '../chunk-
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import '../chunk-
|
|
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":[
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"helpers.mjs"}
|
package/dist/auth/index.js
CHANGED
|
@@ -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) {
|