@xk2800/nextjs-template 0.5.3
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 +339 -0
- package/components/auth/authCard.tsx +92 -0
- package/components/auth/authShell.tsx +46 -0
- package/components/auth/deviceCheck.tsx +52 -0
- package/components/auth/emailPasswordLogin.tsx +136 -0
- package/components/auth/emailPasswordSignup.tsx +178 -0
- package/components/auth/forgotPasswordForm.tsx +66 -0
- package/components/auth/loginErrorRecovery.tsx +117 -0
- package/components/auth/logoutButtons.tsx +26 -0
- package/components/auth/oneTap.tsx +50 -0
- package/components/auth/passkeyLogin.tsx +50 -0
- package/components/auth/resetPasswordForm.tsx +112 -0
- package/components/auth/socialLogin.tsx +51 -0
- package/components/auth/twoFactorVerifyForm.tsx +100 -0
- package/components/dashboard/accountDetailsCard.tsx +58 -0
- package/components/dashboard/activityLogsCard.tsx +141 -0
- package/components/dashboard/activityLogsCardSkeleton.tsx +20 -0
- package/components/dashboard/activityLogsSection.tsx +16 -0
- package/components/dashboard/admin/activityLogsSection.tsx +13 -0
- package/components/dashboard/admin/adminActivityLogsSkeleton.tsx +34 -0
- package/components/dashboard/admin/adminActivityLogsTable.tsx +319 -0
- package/components/dashboard/admin/adminStatsGrid.tsx +30 -0
- package/components/dashboard/admin/adminStatsGridSkeleton.tsx +16 -0
- package/components/dashboard/admin/adminUserActivityLogCard.tsx +53 -0
- package/components/dashboard/admin/adminUserActivityLogTable.tsx +69 -0
- package/components/dashboard/admin/adminUserLoginInfoCard.tsx +50 -0
- package/components/dashboard/admin/adminUserLoginInfoTable.tsx +63 -0
- package/components/dashboard/admin/adminUserSessionsCard.tsx +225 -0
- package/components/dashboard/admin/adminUserStatusCard.tsx +57 -0
- package/components/dashboard/admin/adminUsersSkeleton.tsx +30 -0
- package/components/dashboard/admin/adminUsersTable.tsx +596 -0
- package/components/dashboard/admin/deviceThrottleCard.tsx +59 -0
- package/components/dashboard/admin/deviceThrottleTable.tsx +93 -0
- package/components/dashboard/admin/impersonationLogCard.tsx +73 -0
- package/components/dashboard/admin/newDeviceCard.tsx +35 -0
- package/components/dashboard/admin/newDeviceTable.tsx +71 -0
- package/components/dashboard/admin/revokeAllSessionsButton.tsx +77 -0
- package/components/dashboard/admin/sharedDeviceCard.tsx +42 -0
- package/components/dashboard/admin/sharedDeviceTable.tsx +75 -0
- package/components/dashboard/admin/systemSettingsForm.tsx +200 -0
- package/components/dashboard/admin/systemSettingsSection.tsx +9 -0
- package/components/dashboard/admin/systemSettingsSkeleton.tsx +29 -0
- package/components/dashboard/admin/usersSection.tsx +13 -0
- package/components/dashboard/adminSection.tsx +31 -0
- package/components/dashboard/appSidebar.tsx +170 -0
- package/components/dashboard/dashboardHeader.tsx +85 -0
- package/components/dashboard/heartbeat.tsx +22 -0
- package/components/dashboard/impersonationBanner.tsx +50 -0
- package/components/dashboard/passkeysCard.tsx +187 -0
- package/components/dashboard/profileCard.tsx +42 -0
- package/components/dashboard/sectionErrorFallback.tsx +18 -0
- package/components/dashboard/sessionsCard.tsx +194 -0
- package/components/dashboard/sessionsCardSkeleton.tsx +20 -0
- package/components/dashboard/sessionsSection.tsx +22 -0
- package/components/dashboard/twoFactorCard.tsx +344 -0
- package/components/dashboard/verifyEmailButton.tsx +32 -0
- package/components/email/email-template-2.tsx +44 -0
- package/components/email/email-template-new-device.tsx +80 -0
- package/components/email/email-template-passkey-change.tsx +81 -0
- package/components/email/email-template-reset-password.tsx +56 -0
- package/components/email/email-template-verify-email.tsx +55 -0
- package/components/email/email-template-welcome.tsx +53 -0
- package/components/email/email-template.tsx +33 -0
- package/components/email/send-email-button.tsx +61 -0
- package/components/providers/theme-provider.tsx +9 -0
- package/components/theme/theme-toggle.tsx +53 -0
- package/components/ui/alert-dialog.tsx +141 -0
- package/components/ui/avatar.tsx +53 -0
- package/components/ui/badge.tsx +46 -0
- package/components/ui/button.tsx +64 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/data-table.tsx +302 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +203 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/password-input.tsx +38 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +143 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/switch.tsx +35 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +57 -0
- package/dist/activity/logger.d.mts +18 -0
- package/dist/activity/logger.d.ts +18 -0
- package/dist/activity/logger.js +289 -0
- package/dist/activity/logger.js.map +1 -0
- package/dist/activity/logger.mjs +7 -0
- package/dist/activity/logger.mjs.map +1 -0
- package/dist/activity/queries.d.mts +98 -0
- package/dist/activity/queries.d.ts +98 -0
- package/dist/activity/queries.js +372 -0
- package/dist/activity/queries.js.map +1 -0
- package/dist/activity/queries.mjs +124 -0
- package/dist/activity/queries.mjs.map +1 -0
- package/dist/admin/queries.d.mts +68 -0
- package/dist/admin/queries.d.ts +68 -0
- package/dist/admin/queries.js +450 -0
- package/dist/admin/queries.js.map +1 -0
- package/dist/admin/queries.mjs +143 -0
- package/dist/admin/queries.mjs.map +1 -0
- package/dist/auth/helpers.d.mts +111 -0
- package/dist/auth/helpers.d.ts +111 -0
- package/dist/auth/helpers.js +1023 -0
- package/dist/auth/helpers.js.map +1 -0
- package/dist/auth/helpers.mjs +67 -0
- package/dist/auth/helpers.mjs.map +1 -0
- package/dist/auth/index.d.mts +6589 -0
- package/dist/auth/index.d.ts +6589 -0
- package/dist/auth/index.js +968 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/index.mjs +10 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth-client/index.d.mts +4864 -0
- package/dist/auth-client/index.d.ts +4864 -0
- package/dist/auth-client/index.js +55 -0
- package/dist/auth-client/index.js.map +1 -0
- package/dist/auth-client/index.mjs +49 -0
- package/dist/auth-client/index.mjs.map +1 -0
- package/dist/chunk-2FW7C3KS.mjs +556 -0
- package/dist/chunk-2FW7C3KS.mjs.map +1 -0
- package/dist/chunk-ENGYO4K7.mjs +189 -0
- package/dist/chunk-ENGYO4K7.mjs.map +1 -0
- package/dist/chunk-GTPTVNY2.mjs +40 -0
- package/dist/chunk-GTPTVNY2.mjs.map +1 -0
- package/dist/chunk-HYKUB3A2.mjs +30 -0
- package/dist/chunk-HYKUB3A2.mjs.map +1 -0
- package/dist/chunk-JIYHBWAU.mjs +79 -0
- package/dist/chunk-JIYHBWAU.mjs.map +1 -0
- package/dist/chunk-K6OJCSJS.mjs +41 -0
- package/dist/chunk-K6OJCSJS.mjs.map +1 -0
- package/dist/chunk-N6ECRCHY.mjs +61 -0
- package/dist/chunk-N6ECRCHY.mjs.map +1 -0
- package/dist/chunk-YN4436PK.mjs +33 -0
- package/dist/chunk-YN4436PK.mjs.map +1 -0
- package/dist/config/env.d.mts +15 -0
- package/dist/config/env.d.ts +15 -0
- package/dist/config/env.js +42 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/env.mjs +4 -0
- package/dist/config/env.mjs.map +1 -0
- package/dist/db/index.d.mts +10 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.js +252 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/index.mjs +6 -0
- package/dist/db/index.mjs.map +1 -0
- package/dist/db/schema.d.mts +2 -0
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +186 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/schema.mjs +4 -0
- package/dist/db/schema.mjs.map +1 -0
- package/dist/schema-CgkDv4wJ.d.mts +1758 -0
- package/dist/schema-CgkDv4wJ.d.ts +1758 -0
- package/dist/sessions/queries.d.mts +34 -0
- package/dist/sessions/queries.d.ts +34 -0
- package/dist/sessions/queries.js +268 -0
- package/dist/sessions/queries.js.map +1 -0
- package/dist/sessions/queries.mjs +23 -0
- package/dist/sessions/queries.mjs.map +1 -0
- package/dist/settings/queries.d.mts +20 -0
- package/dist/settings/queries.d.ts +20 -0
- package/dist/settings/queries.js +344 -0
- package/dist/settings/queries.js.map +1 -0
- package/dist/settings/queries.mjs +7 -0
- package/dist/settings/queries.mjs.map +1 -0
- package/dist/types/auth/loginSchema.d.mts +8 -0
- package/dist/types/auth/loginSchema.d.ts +8 -0
- package/dist/types/auth/loginSchema.js +18 -0
- package/dist/types/auth/loginSchema.js.map +1 -0
- package/dist/types/auth/loginSchema.mjs +16 -0
- package/dist/types/auth/loginSchema.mjs.map +1 -0
- package/dist/types/auth/signupSchema.d.mts +9 -0
- package/dist/types/auth/signupSchema.d.ts +9 -0
- package/dist/types/auth/signupSchema.js +21 -0
- package/dist/types/auth/signupSchema.js.map +1 -0
- package/dist/types/auth/signupSchema.mjs +19 -0
- package/dist/types/auth/signupSchema.mjs.map +1 -0
- package/dist/users/queries.d.mts +71 -0
- package/dist/users/queries.d.ts +71 -0
- package/dist/users/queries.js +293 -0
- package/dist/users/queries.js.map +1 -0
- package/dist/users/queries.mjs +47 -0
- package/dist/users/queries.mjs.map +1 -0
- package/lib/auth-client.ts +58 -0
- package/lib/cookie-names.ts +13 -0
- package/lib/device-fingerprint.ts +21 -0
- package/lib/formatters.ts +53 -0
- package/lib/recalculate-columns.ts +75 -0
- package/lib/request-info.ts +78 -0
- package/lib/utils.ts +6 -0
- package/package.json +301 -0
- package/styles/theme.css +124 -0
- package/types/auth/loginSchema.ts +11 -0
- package/types/auth/signupSchema.ts +14 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { parseUserAgent, lookupGeoLocation, WINDOW_MS, isOverLimit } from '../chunk-N6ECRCHY.mjs';
|
|
2
|
+
import { db } from '../chunk-HYKUB3A2.mjs';
|
|
3
|
+
import { users, sessions, authThrottle, deviceFingerprints } from '../chunk-ENGYO4K7.mjs';
|
|
4
|
+
import '../chunk-GTPTVNY2.mjs';
|
|
5
|
+
import { __spreadProps, __spreadValues } from '../chunk-YN4436PK.mjs';
|
|
6
|
+
import 'server-only';
|
|
7
|
+
import { sql, gte, eq, desc } from 'drizzle-orm';
|
|
8
|
+
|
|
9
|
+
// lib/shared-devices.ts
|
|
10
|
+
function groupSharedDevices(rows, minAccounts, limit) {
|
|
11
|
+
const byVisitor = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const r of rows) {
|
|
13
|
+
let d = byVisitor.get(r.visitorId);
|
|
14
|
+
if (!d) {
|
|
15
|
+
d = {
|
|
16
|
+
visitorId: r.visitorId,
|
|
17
|
+
accountCount: 0,
|
|
18
|
+
lastSeenAt: r.firstSeenAt,
|
|
19
|
+
country: r.country,
|
|
20
|
+
city: r.city,
|
|
21
|
+
userAgent: r.userAgent,
|
|
22
|
+
accounts: []
|
|
23
|
+
};
|
|
24
|
+
byVisitor.set(r.visitorId, d);
|
|
25
|
+
}
|
|
26
|
+
d.accountCount++;
|
|
27
|
+
if (r.firstSeenAt > d.lastSeenAt) d.lastSeenAt = r.firstSeenAt;
|
|
28
|
+
d.accounts.push({
|
|
29
|
+
userId: r.userId,
|
|
30
|
+
userName: r.userName,
|
|
31
|
+
userEmail: r.userEmail,
|
|
32
|
+
banned: r.banned,
|
|
33
|
+
firstSeenAt: r.firstSeenAt
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return [...byVisitor.values()].filter((d) => d.accountCount >= minAccounts).sort(
|
|
37
|
+
(a, b) => b.accountCount - a.accountCount || b.lastSeenAt.getTime() - a.lastSeenAt.getTime()
|
|
38
|
+
).slice(0, limit);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// lib/admin-queries.ts
|
|
42
|
+
async function getAdminDashboardStats() {
|
|
43
|
+
var _a, _b, _c, _d;
|
|
44
|
+
const now = /* @__PURE__ */ new Date();
|
|
45
|
+
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
|
|
46
|
+
const [totalUsersResult, newSignupsResult, activeSessionsResult, bannedResult] = await Promise.all([
|
|
47
|
+
db.select({ count: sql`cast(count(*) as integer)` }).from(users),
|
|
48
|
+
db.select({ count: sql`cast(count(*) as integer)` }).from(users).where(gte(users.createdAt, weekAgo)),
|
|
49
|
+
db.select({ count: sql`cast(count(*) as integer)` }).from(sessions).where(sql`${sessions.expiresAt} > ${now}`),
|
|
50
|
+
db.select({ count: sql`cast(count(*) as integer)` }).from(users).where(eq(users.banned, true))
|
|
51
|
+
]);
|
|
52
|
+
return {
|
|
53
|
+
totalUsers: ((_a = totalUsersResult[0]) == null ? void 0 : _a.count) || 0,
|
|
54
|
+
newSignupsThisWeek: ((_b = newSignupsResult[0]) == null ? void 0 : _b.count) || 0,
|
|
55
|
+
activeSessions: ((_c = activeSessionsResult[0]) == null ? void 0 : _c.count) || 0,
|
|
56
|
+
bannedCount: ((_d = bannedResult[0]) == null ? void 0 : _d.count) || 0
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function getAdminStats() {
|
|
60
|
+
var _a, _b, _c;
|
|
61
|
+
const totalUsersResult = await db.select({ count: sql`cast(count(*) as integer)` }).from(users);
|
|
62
|
+
const totalUsers = ((_a = totalUsersResult[0]) == null ? void 0 : _a.count) || 0;
|
|
63
|
+
const now = /* @__PURE__ */ new Date();
|
|
64
|
+
const activeSessionsResult = await db.select({ count: sql`cast(count(*) as integer)` }).from(sessions).where(sql`${sessions.expiresAt} > ${now}`);
|
|
65
|
+
const activeSessions = ((_b = activeSessionsResult[0]) == null ? void 0 : _b.count) || 0;
|
|
66
|
+
const adminCountResult = await db.select({ count: sql`cast(count(*) as integer)` }).from(users).where(sql`${users.role} = 'admin'`);
|
|
67
|
+
const adminCount = ((_c = adminCountResult[0]) == null ? void 0 : _c.count) || 0;
|
|
68
|
+
return {
|
|
69
|
+
totalUsers,
|
|
70
|
+
activeSessions,
|
|
71
|
+
adminCount,
|
|
72
|
+
regularUsers: totalUsers - adminCount
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
var SUSPICIOUS_ATTEMPTS = 2;
|
|
76
|
+
async function getDeviceThrottleActivity(limit = 50) {
|
|
77
|
+
const rows = await db.select().from(authThrottle).where(gte(authThrottle.count, SUSPICIOUS_ATTEMPTS)).orderBy(desc(authThrottle.lastAttemptAt)).limit(limit);
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
return rows.map((r) => {
|
|
80
|
+
const device = parseUserAgent(r.userAgent);
|
|
81
|
+
const geo = lookupGeoLocation(r.ipAddress);
|
|
82
|
+
return {
|
|
83
|
+
fingerprint: r.fingerprint,
|
|
84
|
+
count: r.count,
|
|
85
|
+
windowStart: r.windowStart,
|
|
86
|
+
lastAttemptAt: r.lastAttemptAt,
|
|
87
|
+
ipAddress: r.ipAddress,
|
|
88
|
+
userAgent: r.userAgent,
|
|
89
|
+
lastEmail: r.lastEmail,
|
|
90
|
+
lastKind: r.lastKind,
|
|
91
|
+
os: device.os,
|
|
92
|
+
browser: device.browser,
|
|
93
|
+
deviceType: device.deviceType,
|
|
94
|
+
country: geo.country,
|
|
95
|
+
city: geo.city,
|
|
96
|
+
// Would this device be 429'd on its next attempt right now?
|
|
97
|
+
blocked: isOverLimit(r, now),
|
|
98
|
+
// Is the counter still inside its window (attack ongoing) or has it
|
|
99
|
+
// gone quiet (last burst, awaiting reset on next attempt)?
|
|
100
|
+
windowActive: now - r.windowStart.getTime() < WINDOW_MS
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async function getRecentNewDevices(limit = 20) {
|
|
105
|
+
const rows = await db.select({
|
|
106
|
+
id: deviceFingerprints.id,
|
|
107
|
+
userId: deviceFingerprints.userId,
|
|
108
|
+
userName: users.name,
|
|
109
|
+
userEmail: users.email,
|
|
110
|
+
ipAddress: deviceFingerprints.ipAddress,
|
|
111
|
+
userAgent: deviceFingerprints.userAgent,
|
|
112
|
+
country: deviceFingerprints.country,
|
|
113
|
+
city: deviceFingerprints.city,
|
|
114
|
+
createdAt: deviceFingerprints.createdAt
|
|
115
|
+
}).from(deviceFingerprints).innerJoin(users, eq(deviceFingerprints.userId, users.id)).orderBy(desc(deviceFingerprints.createdAt)).limit(limit);
|
|
116
|
+
return rows.map((r) => {
|
|
117
|
+
const device = parseUserAgent(r.userAgent);
|
|
118
|
+
return __spreadProps(__spreadValues({}, r), {
|
|
119
|
+
os: device.os,
|
|
120
|
+
browser: device.browser,
|
|
121
|
+
deviceType: device.deviceType
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
var MIN_SHARED_ACCOUNTS = 3;
|
|
126
|
+
async function getSharedDeviceAccounts(limit = 20) {
|
|
127
|
+
const rows = await db.select({
|
|
128
|
+
visitorId: deviceFingerprints.visitorId,
|
|
129
|
+
userId: deviceFingerprints.userId,
|
|
130
|
+
userName: users.name,
|
|
131
|
+
userEmail: users.email,
|
|
132
|
+
banned: users.banned,
|
|
133
|
+
firstSeenAt: deviceFingerprints.createdAt,
|
|
134
|
+
userAgent: deviceFingerprints.userAgent,
|
|
135
|
+
country: deviceFingerprints.country,
|
|
136
|
+
city: deviceFingerprints.city
|
|
137
|
+
}).from(deviceFingerprints).innerJoin(users, eq(deviceFingerprints.userId, users.id)).orderBy(desc(deviceFingerprints.createdAt));
|
|
138
|
+
return groupSharedDevices(rows, MIN_SHARED_ACCOUNTS, limit).map((d) => __spreadValues(__spreadValues({}, d), parseUserAgent(d.userAgent)));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { getAdminDashboardStats, getAdminStats, getDeviceThrottleActivity, getRecentNewDevices, getSharedDeviceAccounts };
|
|
142
|
+
//# sourceMappingURL=queries.mjs.map
|
|
143
|
+
//# sourceMappingURL=queries.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../lib/shared-devices.ts","../../lib/admin-queries.ts"],"names":[],"mappings":"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","file":"queries.mjs","sourcesContent":["// Pure grouping for the admin \"Shared devices\" card — no DB, no `server-only`,\n// so it stays unit-testable. Consumed by lib/admin-queries.ts.\n\nexport type SharedDeviceAccountRow = {\n visitorId: string\n userId: string\n userName: string\n userEmail: string\n banned: boolean\n firstSeenAt: Date\n userAgent: string | null\n country: string | null\n city: string | null\n}\n\nexport type SharedDevice = {\n visitorId: string\n accountCount: number\n lastSeenAt: Date\n country: string | null\n city: string | null\n userAgent: string | null\n accounts: {\n userId: string\n userName: string\n userEmail: string\n banned: boolean\n firstSeenAt: Date\n }[]\n}\n\n// Collapse flat (account, device) rows into one entry per device fingerprint,\n// keep only devices reached by `minAccounts`+ accounts (the multi-account /\n// trial-abuse signal), most-accounts device first. Device context (UA/geo) is\n// taken from whichever row for that visitorId is seen first — pass rows newest\n// first if you want the most recent context.\nexport function groupSharedDevices(\n rows: SharedDeviceAccountRow[],\n minAccounts: number,\n limit: number,\n): SharedDevice[] {\n const byVisitor = new Map<string, SharedDevice>()\n\n for (const r of rows) {\n let d = byVisitor.get(r.visitorId)\n if (!d) {\n d = {\n visitorId: r.visitorId,\n accountCount: 0,\n lastSeenAt: r.firstSeenAt,\n country: r.country,\n city: r.city,\n userAgent: r.userAgent,\n accounts: [],\n }\n byVisitor.set(r.visitorId, d)\n }\n d.accountCount++\n if (r.firstSeenAt > d.lastSeenAt) d.lastSeenAt = r.firstSeenAt\n d.accounts.push({\n userId: r.userId,\n userName: r.userName,\n userEmail: r.userEmail,\n banned: r.banned,\n firstSeenAt: r.firstSeenAt,\n })\n }\n\n return [...byVisitor.values()]\n .filter((d) => d.accountCount >= minAccounts)\n .sort(\n (a, b) =>\n b.accountCount - a.accountCount || b.lastSeenAt.getTime() - a.lastSeenAt.getTime(),\n )\n .slice(0, limit)\n}\n","import \"server-only\"\nimport { db } from \"../server/db\"\nimport { users, sessions, authThrottle, deviceFingerprints } from \"../server/db/schema\"\nimport { sql, gte, eq, desc } from \"drizzle-orm\"\nimport { parseUserAgent, lookupGeoLocation } from \"./request-info\"\nimport { WINDOW_MS, isOverLimit } from \"./auth-throttle-limits\"\nimport { groupSharedDevices } from \"./shared-devices\"\n\n// Powers the admin panel's landing page (/dashboard/admin) — kept separate\n// from getAdminStats (the compact overview embedded in the main dashboard)\n// since the two surfaces show different metrics.\nexport async function getAdminDashboardStats() {\n const now = new Date()\n const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)\n\n const [totalUsersResult, newSignupsResult, activeSessionsResult, bannedResult] = await Promise.all([\n db.select({ count: sql<number>`cast(count(*) as integer)` }).from(users),\n db\n .select({ count: sql<number>`cast(count(*) as integer)` })\n .from(users)\n .where(gte(users.createdAt, weekAgo)),\n db\n .select({ count: sql<number>`cast(count(*) as integer)` })\n .from(sessions)\n .where(sql`${sessions.expiresAt} > ${now}`),\n db\n .select({ count: sql<number>`cast(count(*) as integer)` })\n .from(users)\n .where(eq(users.banned, true)),\n ])\n\n return {\n totalUsers: totalUsersResult[0]?.count || 0,\n newSignupsThisWeek: newSignupsResult[0]?.count || 0,\n activeSessions: activeSessionsResult[0]?.count || 0,\n bannedCount: bannedResult[0]?.count || 0,\n }\n}\n\nexport async function getAdminStats() {\n const totalUsersResult = await db\n .select({ count: sql<number>`cast(count(*) as integer)` })\n .from(users)\n const totalUsers = totalUsersResult[0]?.count || 0\n\n const now = new Date()\n const activeSessionsResult = await db\n .select({ count: sql<number>`cast(count(*) as integer)` })\n .from(sessions)\n .where(sql`${sessions.expiresAt} > ${now}`)\n const activeSessions = activeSessionsResult[0]?.count || 0\n\n const adminCountResult = await db\n .select({ count: sql<number>`cast(count(*) as integer)` })\n .from(users)\n .where(sql`${users.role} = 'admin'`)\n const adminCount = adminCountResult[0]?.count || 0\n\n return {\n totalUsers,\n activeSessions,\n adminCount,\n regularUsers: totalUsers - adminCount,\n }\n}\n\n// Rows below this are noise — a single new signup, or one login typo that\n// then succeeded (a success deletes the row, so a lingering count of 1 is\n// almost always a lone signup).\nconst SUSPICIOUS_ATTEMPTS = 2\n\nexport type DeviceThrottleRow = Awaited<ReturnType<typeof getDeviceThrottleActivity>>[number]\n\n// Powers the admin \"Device throttle\" card. One row per device fingerprint\n// that's currently accumulating failed sign-ins / sign-ups, newest first,\n// with everything an admin needs to block it at the edge (IP) or in-app\n// (fingerprint) — parsed device + geo included for context.\nexport async function getDeviceThrottleActivity(limit = 50) {\n const rows = await db\n .select()\n .from(authThrottle)\n .where(gte(authThrottle.count, SUSPICIOUS_ATTEMPTS))\n .orderBy(desc(authThrottle.lastAttemptAt))\n .limit(limit)\n\n const now = Date.now()\n return rows.map((r) => {\n const device = parseUserAgent(r.userAgent)\n const geo = lookupGeoLocation(r.ipAddress)\n return {\n fingerprint: r.fingerprint,\n count: r.count,\n windowStart: r.windowStart,\n lastAttemptAt: r.lastAttemptAt,\n ipAddress: r.ipAddress,\n userAgent: r.userAgent,\n lastEmail: r.lastEmail,\n lastKind: r.lastKind,\n os: device.os,\n browser: device.browser,\n deviceType: device.deviceType,\n country: geo.country,\n city: geo.city,\n // Would this device be 429'd on its next attempt right now?\n blocked: isOverLimit(r, now),\n // Is the counter still inside its window (attack ongoing) or has it\n // gone quiet (last burst, awaiting reset on next attempt)?\n windowActive: now - r.windowStart.getTime() < WINDOW_MS,\n }\n })\n}\n\nexport type RecentNewDeviceRow = Awaited<ReturnType<typeof getRecentNewDevices>>[number]\n\n// Powers the admin \"New devices\" card. One row per (account, device) pair the\n// first time that account is seen on that device — written by\n// app/api/device-check/route.ts, newest first. The row links to the user's\n// admin detail page, where sessions can be revoked if the sign-in wasn't them.\nexport async function getRecentNewDevices(limit = 20) {\n const rows = await db\n .select({\n id: deviceFingerprints.id,\n userId: deviceFingerprints.userId,\n userName: users.name,\n userEmail: users.email,\n ipAddress: deviceFingerprints.ipAddress,\n userAgent: deviceFingerprints.userAgent,\n country: deviceFingerprints.country,\n city: deviceFingerprints.city,\n createdAt: deviceFingerprints.createdAt,\n })\n .from(deviceFingerprints)\n .innerJoin(users, eq(deviceFingerprints.userId, users.id))\n .orderBy(desc(deviceFingerprints.createdAt))\n .limit(limit)\n\n return rows.map((r) => {\n const device = parseUserAgent(r.userAgent)\n return {\n ...r,\n os: device.os,\n browser: device.browser,\n deviceType: device.deviceType,\n }\n })\n}\n\n// A device fingerprint (FingerprintJS visitorId) is normally 1:1 with an\n// account, or 1:few (personal + work login on one laptop). One visitorId behind\n// this many accounts is the multi-account / trial-abuse smell.\nconst MIN_SHARED_ACCOUNTS = 3\n\nexport type SharedDeviceRow = Awaited<ReturnType<typeof getSharedDeviceAccounts>>[number]\n\n// Powers the admin \"Shared devices\" card. One row per device fingerprint that\n// MIN_SHARED_ACCOUNTS+ distinct accounts have signed in from, most-shared first,\n// each with its full account list so an admin can open and ban them. The\n// device_fingerprint unique (userId, visitorId) constraint means a plain row\n// count per visitorId already IS the distinct-account count.\nexport async function getSharedDeviceAccounts(limit = 20) {\n // ponytail: pulls the whole device_fingerprint table each call and groups in\n // JS — fine for a template. If this table gets large, add an index on\n // visitorId and push the \"N+ accounts\" filter into a HAVING subquery.\n const rows = await db\n .select({\n visitorId: deviceFingerprints.visitorId,\n userId: deviceFingerprints.userId,\n userName: users.name,\n userEmail: users.email,\n banned: users.banned,\n firstSeenAt: deviceFingerprints.createdAt,\n userAgent: deviceFingerprints.userAgent,\n country: deviceFingerprints.country,\n city: deviceFingerprints.city,\n })\n .from(deviceFingerprints)\n .innerJoin(users, eq(deviceFingerprints.userId, users.id))\n .orderBy(desc(deviceFingerprints.createdAt))\n\n return groupSharedDevices(rows, MIN_SHARED_ACCOUNTS, limit).map((d) => ({\n ...d,\n ...parseUserAgent(d.userAgent),\n }))\n}\n"]}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get current session (server-side only)
|
|
3
|
+
* Returns session data or null if not authenticated
|
|
4
|
+
*
|
|
5
|
+
* Wrapped in React's cache() so the layout and page (both of which call
|
|
6
|
+
* requireAuth per request) share one auth.api.getSession call instead of two.
|
|
7
|
+
*/
|
|
8
|
+
declare const getCurrentSession: () => Promise<{
|
|
9
|
+
session: {
|
|
10
|
+
id: string;
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
updatedAt: Date;
|
|
13
|
+
userId: string;
|
|
14
|
+
expiresAt: Date;
|
|
15
|
+
token: string;
|
|
16
|
+
ipAddress?: string | null | undefined | undefined;
|
|
17
|
+
userAgent?: string | null | undefined | undefined;
|
|
18
|
+
impersonatedBy?: string | null | undefined;
|
|
19
|
+
};
|
|
20
|
+
user: {
|
|
21
|
+
id: string;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
updatedAt: Date;
|
|
24
|
+
email: string;
|
|
25
|
+
emailVerified: boolean;
|
|
26
|
+
name: string;
|
|
27
|
+
image?: string | null | undefined | undefined;
|
|
28
|
+
role: string | null | undefined;
|
|
29
|
+
banned: boolean | null | undefined;
|
|
30
|
+
banExpires?: Date | null | undefined;
|
|
31
|
+
banReason?: string | null | undefined;
|
|
32
|
+
twoFactorEnabled: boolean | null | undefined;
|
|
33
|
+
};
|
|
34
|
+
} | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Require authentication - redirect to login if not authenticated
|
|
37
|
+
* Use in server components/layouts that need protection
|
|
38
|
+
*
|
|
39
|
+
* Wrapped in cache() so the layout, page, and any admin layout that all call
|
|
40
|
+
* requireAuth per request share one banned-status DB lookup instead of several.
|
|
41
|
+
*/
|
|
42
|
+
declare const requireAuth: () => Promise<{
|
|
43
|
+
session: {
|
|
44
|
+
id: string;
|
|
45
|
+
createdAt: Date;
|
|
46
|
+
updatedAt: Date;
|
|
47
|
+
userId: string;
|
|
48
|
+
expiresAt: Date;
|
|
49
|
+
token: string;
|
|
50
|
+
ipAddress?: string | null | undefined | undefined;
|
|
51
|
+
userAgent?: string | null | undefined | undefined;
|
|
52
|
+
impersonatedBy?: string | null | undefined;
|
|
53
|
+
};
|
|
54
|
+
user: {
|
|
55
|
+
id: string;
|
|
56
|
+
createdAt: Date;
|
|
57
|
+
updatedAt: Date;
|
|
58
|
+
email: string;
|
|
59
|
+
emailVerified: boolean;
|
|
60
|
+
name: string;
|
|
61
|
+
image?: string | null | undefined | undefined;
|
|
62
|
+
role: string | null | undefined;
|
|
63
|
+
banned: boolean | null | undefined;
|
|
64
|
+
banExpires?: Date | null | undefined;
|
|
65
|
+
banReason?: string | null | undefined;
|
|
66
|
+
twoFactorEnabled: boolean | null | undefined;
|
|
67
|
+
};
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Check if user has specific role
|
|
71
|
+
*/
|
|
72
|
+
declare function hasRole(userRole: string | null | undefined, requiredRole: 'user' | 'admin'): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Require specific role - throw error if user doesn't have it
|
|
75
|
+
*/
|
|
76
|
+
declare function requireRole(requiredRole: 'user' | 'admin'): Promise<{
|
|
77
|
+
session: {
|
|
78
|
+
id: string;
|
|
79
|
+
createdAt: Date;
|
|
80
|
+
updatedAt: Date;
|
|
81
|
+
userId: string;
|
|
82
|
+
expiresAt: Date;
|
|
83
|
+
token: string;
|
|
84
|
+
ipAddress?: string | null | undefined | undefined;
|
|
85
|
+
userAgent?: string | null | undefined | undefined;
|
|
86
|
+
impersonatedBy?: string | null | undefined;
|
|
87
|
+
};
|
|
88
|
+
user: {
|
|
89
|
+
id: string;
|
|
90
|
+
createdAt: Date;
|
|
91
|
+
updatedAt: Date;
|
|
92
|
+
email: string;
|
|
93
|
+
emailVerified: boolean;
|
|
94
|
+
name: string;
|
|
95
|
+
image?: string | null | undefined | undefined;
|
|
96
|
+
role: string | null | undefined;
|
|
97
|
+
banned: boolean | null | undefined;
|
|
98
|
+
banExpires?: Date | null | undefined;
|
|
99
|
+
banReason?: string | null | undefined;
|
|
100
|
+
twoFactorEnabled: boolean | null | undefined;
|
|
101
|
+
};
|
|
102
|
+
}>;
|
|
103
|
+
/**
|
|
104
|
+
* Normalize a callbackUrl query param to a same-origin path before it's ever
|
|
105
|
+
* passed to redirect() or an auth form's callbackURL. Guards against open
|
|
106
|
+
* redirects (e.g. /login?callbackUrl=https://evil.com) — anything that
|
|
107
|
+
* doesn't resolve to this app's own origin falls back to /dashboard.
|
|
108
|
+
*/
|
|
109
|
+
declare function normalizeCallbackUrl(value?: string | string[]): string;
|
|
110
|
+
|
|
111
|
+
export { getCurrentSession, hasRole, normalizeCallbackUrl, requireAuth, requireRole };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get current session (server-side only)
|
|
3
|
+
* Returns session data or null if not authenticated
|
|
4
|
+
*
|
|
5
|
+
* Wrapped in React's cache() so the layout and page (both of which call
|
|
6
|
+
* requireAuth per request) share one auth.api.getSession call instead of two.
|
|
7
|
+
*/
|
|
8
|
+
declare const getCurrentSession: () => Promise<{
|
|
9
|
+
session: {
|
|
10
|
+
id: string;
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
updatedAt: Date;
|
|
13
|
+
userId: string;
|
|
14
|
+
expiresAt: Date;
|
|
15
|
+
token: string;
|
|
16
|
+
ipAddress?: string | null | undefined | undefined;
|
|
17
|
+
userAgent?: string | null | undefined | undefined;
|
|
18
|
+
impersonatedBy?: string | null | undefined;
|
|
19
|
+
};
|
|
20
|
+
user: {
|
|
21
|
+
id: string;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
updatedAt: Date;
|
|
24
|
+
email: string;
|
|
25
|
+
emailVerified: boolean;
|
|
26
|
+
name: string;
|
|
27
|
+
image?: string | null | undefined | undefined;
|
|
28
|
+
role: string | null | undefined;
|
|
29
|
+
banned: boolean | null | undefined;
|
|
30
|
+
banExpires?: Date | null | undefined;
|
|
31
|
+
banReason?: string | null | undefined;
|
|
32
|
+
twoFactorEnabled: boolean | null | undefined;
|
|
33
|
+
};
|
|
34
|
+
} | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Require authentication - redirect to login if not authenticated
|
|
37
|
+
* Use in server components/layouts that need protection
|
|
38
|
+
*
|
|
39
|
+
* Wrapped in cache() so the layout, page, and any admin layout that all call
|
|
40
|
+
* requireAuth per request share one banned-status DB lookup instead of several.
|
|
41
|
+
*/
|
|
42
|
+
declare const requireAuth: () => Promise<{
|
|
43
|
+
session: {
|
|
44
|
+
id: string;
|
|
45
|
+
createdAt: Date;
|
|
46
|
+
updatedAt: Date;
|
|
47
|
+
userId: string;
|
|
48
|
+
expiresAt: Date;
|
|
49
|
+
token: string;
|
|
50
|
+
ipAddress?: string | null | undefined | undefined;
|
|
51
|
+
userAgent?: string | null | undefined | undefined;
|
|
52
|
+
impersonatedBy?: string | null | undefined;
|
|
53
|
+
};
|
|
54
|
+
user: {
|
|
55
|
+
id: string;
|
|
56
|
+
createdAt: Date;
|
|
57
|
+
updatedAt: Date;
|
|
58
|
+
email: string;
|
|
59
|
+
emailVerified: boolean;
|
|
60
|
+
name: string;
|
|
61
|
+
image?: string | null | undefined | undefined;
|
|
62
|
+
role: string | null | undefined;
|
|
63
|
+
banned: boolean | null | undefined;
|
|
64
|
+
banExpires?: Date | null | undefined;
|
|
65
|
+
banReason?: string | null | undefined;
|
|
66
|
+
twoFactorEnabled: boolean | null | undefined;
|
|
67
|
+
};
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* Check if user has specific role
|
|
71
|
+
*/
|
|
72
|
+
declare function hasRole(userRole: string | null | undefined, requiredRole: 'user' | 'admin'): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Require specific role - throw error if user doesn't have it
|
|
75
|
+
*/
|
|
76
|
+
declare function requireRole(requiredRole: 'user' | 'admin'): Promise<{
|
|
77
|
+
session: {
|
|
78
|
+
id: string;
|
|
79
|
+
createdAt: Date;
|
|
80
|
+
updatedAt: Date;
|
|
81
|
+
userId: string;
|
|
82
|
+
expiresAt: Date;
|
|
83
|
+
token: string;
|
|
84
|
+
ipAddress?: string | null | undefined | undefined;
|
|
85
|
+
userAgent?: string | null | undefined | undefined;
|
|
86
|
+
impersonatedBy?: string | null | undefined;
|
|
87
|
+
};
|
|
88
|
+
user: {
|
|
89
|
+
id: string;
|
|
90
|
+
createdAt: Date;
|
|
91
|
+
updatedAt: Date;
|
|
92
|
+
email: string;
|
|
93
|
+
emailVerified: boolean;
|
|
94
|
+
name: string;
|
|
95
|
+
image?: string | null | undefined | undefined;
|
|
96
|
+
role: string | null | undefined;
|
|
97
|
+
banned: boolean | null | undefined;
|
|
98
|
+
banExpires?: Date | null | undefined;
|
|
99
|
+
banReason?: string | null | undefined;
|
|
100
|
+
twoFactorEnabled: boolean | null | undefined;
|
|
101
|
+
};
|
|
102
|
+
}>;
|
|
103
|
+
/**
|
|
104
|
+
* Normalize a callbackUrl query param to a same-origin path before it's ever
|
|
105
|
+
* passed to redirect() or an auth form's callbackURL. Guards against open
|
|
106
|
+
* redirects (e.g. /login?callbackUrl=https://evil.com) — anything that
|
|
107
|
+
* doesn't resolve to this app's own origin falls back to /dashboard.
|
|
108
|
+
*/
|
|
109
|
+
declare function normalizeCallbackUrl(value?: string | string[]): string;
|
|
110
|
+
|
|
111
|
+
export { getCurrentSession, hasRole, normalizeCallbackUrl, requireAuth, requireRole };
|