@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,556 @@
|
|
|
1
|
+
import { logActivity } from './chunk-K6OJCSJS.mjs';
|
|
2
|
+
import { getClientIp, parseUserAgent, lookupGeoLocation, formatLocation, formatDeviceInfo, WINDOW_MS, isOverLimit, MAX_IP } from './chunk-N6ECRCHY.mjs';
|
|
3
|
+
import { getEffectiveAuthFlags } from './chunk-JIYHBWAU.mjs';
|
|
4
|
+
import { db } from './chunk-HYKUB3A2.mjs';
|
|
5
|
+
import { users, sessions, passkeys, twoFactors, verifications, accounts, authThrottle } from './chunk-ENGYO4K7.mjs';
|
|
6
|
+
import { config } from './chunk-GTPTVNY2.mjs';
|
|
7
|
+
import { __spreadValues, __spreadProps, __require } from './chunk-YN4436PK.mjs';
|
|
8
|
+
import 'server-only';
|
|
9
|
+
import { betterAuth } from 'better-auth';
|
|
10
|
+
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
|
11
|
+
import { eq, sql } from 'drizzle-orm';
|
|
12
|
+
import bcrypt from 'bcrypt';
|
|
13
|
+
import { oneTap, admin, twoFactor } from 'better-auth/plugins';
|
|
14
|
+
import { passkey } from 'better-auth/plugins/passkey';
|
|
15
|
+
import { createAuthMiddleware, APIError, getSessionFromCtx } from 'better-auth/api';
|
|
16
|
+
import { Resend } from 'resend';
|
|
17
|
+
import { Html, Head, Tailwind, Body, Container, Section, Heading, Text, Button, Link } from '@react-email/components';
|
|
18
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
19
|
+
|
|
20
|
+
// lib/cookie-names.ts
|
|
21
|
+
var LOGIN_REFERRER_COOKIE = "login_referrer";
|
|
22
|
+
var DEVICE_FINGERPRINT_HEADER = "x-device-fingerprint";
|
|
23
|
+
async function isDeviceThrottled(fingerprint, max) {
|
|
24
|
+
try {
|
|
25
|
+
const [row] = await db.select({ count: authThrottle.count, windowStart: authThrottle.windowStart }).from(authThrottle).where(eq(authThrottle.fingerprint, fingerprint)).limit(1);
|
|
26
|
+
return isOverLimit(row, Date.now(), max);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("auth-throttle read failed, allowing request", error);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function recordDeviceAttempt(fingerprint, meta) {
|
|
33
|
+
const now = /* @__PURE__ */ new Date();
|
|
34
|
+
const cutoff = new Date(now.getTime() - WINDOW_MS);
|
|
35
|
+
const seen = {
|
|
36
|
+
lastAttemptAt: now,
|
|
37
|
+
ipAddress: meta.ipAddress,
|
|
38
|
+
userAgent: meta.userAgent,
|
|
39
|
+
lastEmail: meta.email,
|
|
40
|
+
lastKind: meta.kind
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
await db.insert(authThrottle).values(__spreadValues({ fingerprint, count: 1, windowStart: now }, seen)).onConflictDoUpdate({
|
|
44
|
+
target: authThrottle.fingerprint,
|
|
45
|
+
set: __spreadValues({
|
|
46
|
+
count: sql`case when ${authThrottle.windowStart} < ${cutoff} then 1 else ${authThrottle.count} + 1 end`,
|
|
47
|
+
windowStart: sql`case when ${authThrottle.windowStart} < ${cutoff} then ${now} else ${authThrottle.windowStart} end`
|
|
48
|
+
}, seen)
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("auth-throttle write failed", error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function clearDeviceThrottle(fingerprint) {
|
|
55
|
+
try {
|
|
56
|
+
await db.delete(authThrottle).where(eq(authThrottle.fingerprint, fingerprint));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("auth-throttle clear failed", error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
var EMAIL_FROM = "Acme <onboarding@xavierkhew.xyz>";
|
|
62
|
+
var resend;
|
|
63
|
+
function getResend() {
|
|
64
|
+
if (!resend) resend = new Resend(config.RESEND_API_KEY);
|
|
65
|
+
return resend;
|
|
66
|
+
}
|
|
67
|
+
function EmailTemplateResetPassword({ firstName, url }) {
|
|
68
|
+
return /* @__PURE__ */ jsxs(Html, { children: [
|
|
69
|
+
/* @__PURE__ */ jsx(Head, {}),
|
|
70
|
+
/* @__PURE__ */ jsx(Body, { style: { backgroundColor: "#ffffff", fontFamily: "Arial, sans-serif" }, children: /* @__PURE__ */ jsx(Container, { style: { margin: "0 auto", padding: "20px 0 48px", maxWidth: "560px" }, children: /* @__PURE__ */ jsxs(Section, { style: { backgroundColor: "#f8f9fa", borderRadius: "12px", padding: "40px", textAlign: "center" }, children: [
|
|
71
|
+
/* @__PURE__ */ jsx(Heading, { style: { fontSize: "28px", fontWeight: "bold", marginBottom: "12px", color: "#1a1a1a" }, children: "Reset your password" }),
|
|
72
|
+
/* @__PURE__ */ jsxs(Text, { style: { fontSize: "16px", color: "#666666", marginBottom: "32px", lineHeight: "24px" }, children: [
|
|
73
|
+
"Hi ",
|
|
74
|
+
firstName,
|
|
75
|
+
", we got a request to reset your password. This link expires in an hour \u2014 if you didn't ask for this, you can safely ignore this email."
|
|
76
|
+
] }),
|
|
77
|
+
/* @__PURE__ */ jsx(Section, { style: { marginBottom: "32px" }, children: /* @__PURE__ */ jsx(
|
|
78
|
+
Button,
|
|
79
|
+
{
|
|
80
|
+
href: url,
|
|
81
|
+
style: {
|
|
82
|
+
backgroundColor: "#000000",
|
|
83
|
+
color: "#ffffff",
|
|
84
|
+
padding: "12px 32px",
|
|
85
|
+
borderRadius: "6px",
|
|
86
|
+
textDecoration: "none",
|
|
87
|
+
fontSize: "16px",
|
|
88
|
+
fontWeight: "bold"
|
|
89
|
+
},
|
|
90
|
+
children: "Reset password"
|
|
91
|
+
}
|
|
92
|
+
) }),
|
|
93
|
+
/* @__PURE__ */ jsxs(Text, { style: { fontSize: "13px", color: "#999999", lineHeight: "20px", wordBreak: "break-all" }, children: [
|
|
94
|
+
"Or paste this link into your browser:",
|
|
95
|
+
/* @__PURE__ */ jsx("br", {}),
|
|
96
|
+
/* @__PURE__ */ jsx("a", { href: url, style: { color: "#000000", textDecoration: "underline" }, children: url })
|
|
97
|
+
] })
|
|
98
|
+
] }) }) })
|
|
99
|
+
] });
|
|
100
|
+
}
|
|
101
|
+
function EmailTemplateVerifyEmail({ firstName, url }) {
|
|
102
|
+
return /* @__PURE__ */ jsxs(Html, { children: [
|
|
103
|
+
/* @__PURE__ */ jsx(Head, {}),
|
|
104
|
+
/* @__PURE__ */ jsx(Tailwind, { children: /* @__PURE__ */ jsx(Body, { className: "bg-white font-sans", children: /* @__PURE__ */ jsx(Container, { className: "mx-auto max-w-[560px] px-0 pt-5 pb-12", children: /* @__PURE__ */ jsxs(Section, { className: "rounded-xl bg-[#f8f9fa] p-10 text-center", children: [
|
|
105
|
+
/* @__PURE__ */ jsx(Heading, { className: "mb-3 text-[28px] font-bold text-[#1a1a1a]", children: "Confirm your email" }),
|
|
106
|
+
/* @__PURE__ */ jsxs(Text, { className: "mb-8 text-base leading-6 text-[#666666]", children: [
|
|
107
|
+
"Hi ",
|
|
108
|
+
firstName,
|
|
109
|
+
", click the button below to verify this email address. If you didn't create an account, you can safely ignore this email."
|
|
110
|
+
] }),
|
|
111
|
+
/* @__PURE__ */ jsx(Section, { className: "mb-8", children: /* @__PURE__ */ jsx(
|
|
112
|
+
Button,
|
|
113
|
+
{
|
|
114
|
+
href: url,
|
|
115
|
+
className: "rounded-md bg-black px-8 py-3 text-base font-bold text-white no-underline",
|
|
116
|
+
children: "Verify email"
|
|
117
|
+
}
|
|
118
|
+
) }),
|
|
119
|
+
/* @__PURE__ */ jsxs(Text, { className: "text-[13px] leading-5 break-all text-[#999999]", children: [
|
|
120
|
+
"Or paste this link into your browser:",
|
|
121
|
+
/* @__PURE__ */ jsx("br", {}),
|
|
122
|
+
/* @__PURE__ */ jsx(Link, { href: url, className: "text-black underline", children: url })
|
|
123
|
+
] })
|
|
124
|
+
] }) }) }) })
|
|
125
|
+
] });
|
|
126
|
+
}
|
|
127
|
+
function EmailTemplatePasskeyChange({
|
|
128
|
+
firstName,
|
|
129
|
+
action,
|
|
130
|
+
device,
|
|
131
|
+
location,
|
|
132
|
+
ipAddress,
|
|
133
|
+
when,
|
|
134
|
+
manageUrl
|
|
135
|
+
}) {
|
|
136
|
+
const verb = action === "added" ? "added to" : "removed from";
|
|
137
|
+
return /* @__PURE__ */ jsxs(Html, { children: [
|
|
138
|
+
/* @__PURE__ */ jsx(Head, {}),
|
|
139
|
+
/* @__PURE__ */ jsx(Tailwind, { children: /* @__PURE__ */ jsx(Body, { className: "bg-white font-sans", children: /* @__PURE__ */ jsx(Container, { className: "mx-auto max-w-[560px] px-0 pt-5 pb-12", children: /* @__PURE__ */ jsxs(Section, { className: "rounded-xl bg-[#f8f9fa] p-10", children: [
|
|
140
|
+
/* @__PURE__ */ jsxs(Heading, { className: "mb-3 text-center text-2xl font-bold text-[#1a1a1a]", children: [
|
|
141
|
+
"A passkey was ",
|
|
142
|
+
action
|
|
143
|
+
] }),
|
|
144
|
+
/* @__PURE__ */ jsxs(Text, { className: "mb-6 text-base leading-6 text-[#666666]", children: [
|
|
145
|
+
"Hi ",
|
|
146
|
+
firstName,
|
|
147
|
+
", a passkey was just ",
|
|
148
|
+
verb,
|
|
149
|
+
" your account."
|
|
150
|
+
] }),
|
|
151
|
+
/* @__PURE__ */ jsxs(Section, { className: "mb-6 rounded-lg bg-white p-5", children: [
|
|
152
|
+
/* @__PURE__ */ jsxs(Text, { className: "m-0 mb-2 text-sm leading-5 text-[#1a1a1a]", children: [
|
|
153
|
+
/* @__PURE__ */ jsx("strong", { children: "Device:" }),
|
|
154
|
+
" ",
|
|
155
|
+
device
|
|
156
|
+
] }),
|
|
157
|
+
/* @__PURE__ */ jsxs(Text, { className: "m-0 mb-2 text-sm leading-5 text-[#1a1a1a]", children: [
|
|
158
|
+
/* @__PURE__ */ jsx("strong", { children: "Approx. location:" }),
|
|
159
|
+
" ",
|
|
160
|
+
location
|
|
161
|
+
] }),
|
|
162
|
+
/* @__PURE__ */ jsxs(Text, { className: "m-0 mb-2 text-sm leading-5 text-[#1a1a1a]", children: [
|
|
163
|
+
/* @__PURE__ */ jsx("strong", { children: "IP address:" }),
|
|
164
|
+
" ",
|
|
165
|
+
ipAddress
|
|
166
|
+
] }),
|
|
167
|
+
/* @__PURE__ */ jsxs(Text, { className: "m-0 text-sm leading-5 text-[#1a1a1a]", children: [
|
|
168
|
+
/* @__PURE__ */ jsx("strong", { children: "When:" }),
|
|
169
|
+
" ",
|
|
170
|
+
when
|
|
171
|
+
] })
|
|
172
|
+
] }),
|
|
173
|
+
manageUrl && /* @__PURE__ */ jsx(Section, { className: "mb-6 text-center", children: /* @__PURE__ */ jsx(
|
|
174
|
+
Button,
|
|
175
|
+
{
|
|
176
|
+
href: manageUrl,
|
|
177
|
+
className: "rounded-lg bg-[#1a1a1a] px-6 py-3 text-sm font-bold text-white no-underline",
|
|
178
|
+
children: "Review your passkeys"
|
|
179
|
+
}
|
|
180
|
+
) }),
|
|
181
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm leading-[22px] text-[#666666]", children: "If this was you, no action is needed. If you don't recognize it, change your password now and review your passkeys and active sessions." })
|
|
182
|
+
] }) }) }) })
|
|
183
|
+
] });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// server/auth.ts
|
|
187
|
+
async function logFailedLogin(ctx) {
|
|
188
|
+
var _a, _b, _c;
|
|
189
|
+
try {
|
|
190
|
+
const raw = (_a = ctx.body) == null ? void 0 : _a.email;
|
|
191
|
+
const email = typeof raw === "string" ? raw.toLowerCase() : null;
|
|
192
|
+
if (!email) return;
|
|
193
|
+
const [user] = await db.select({ id: users.id }).from(users).where(eq(users.email, email)).limit(1);
|
|
194
|
+
if (!user) return;
|
|
195
|
+
await logActivity({
|
|
196
|
+
userId: user.id,
|
|
197
|
+
action: "login_failed",
|
|
198
|
+
description: "Failed login attempt",
|
|
199
|
+
ipAddress: getClientIp(ctx.headers),
|
|
200
|
+
userAgent: (_c = (_b = ctx.headers) == null ? void 0 : _b.get("user-agent")) != null ? _c : null
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error("Failed to log login_failed activity", error);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function notifyPasskeyChange(ctx, action) {
|
|
207
|
+
var _a, _b, _c, _d;
|
|
208
|
+
try {
|
|
209
|
+
const session = await getSessionFromCtx(ctx);
|
|
210
|
+
const user = session == null ? void 0 : session.user;
|
|
211
|
+
if (!user) return;
|
|
212
|
+
const ip = getClientIp(ctx.headers);
|
|
213
|
+
await logActivity({
|
|
214
|
+
userId: user.id,
|
|
215
|
+
action: action === "added" ? "passkey_added" : "passkey_removed",
|
|
216
|
+
description: `Passkey ${action}`,
|
|
217
|
+
ipAddress: ip,
|
|
218
|
+
userAgent: (_b = (_a = ctx.headers) == null ? void 0 : _a.get("user-agent")) != null ? _b : null
|
|
219
|
+
});
|
|
220
|
+
if (!config.RESEND_API_KEY) return;
|
|
221
|
+
await getResend().emails.send({
|
|
222
|
+
from: EMAIL_FROM,
|
|
223
|
+
to: [user.email],
|
|
224
|
+
subject: action === "added" ? "A passkey was added to your account" : "A passkey was removed from your account",
|
|
225
|
+
react: EmailTemplatePasskeyChange({
|
|
226
|
+
firstName: user.name || "there",
|
|
227
|
+
action,
|
|
228
|
+
device: formatDeviceInfo(parseUserAgent((_d = (_c = ctx.headers) == null ? void 0 : _c.get("user-agent")) != null ? _d : null)),
|
|
229
|
+
location: formatLocation(lookupGeoLocation(ip)),
|
|
230
|
+
ipAddress: ip || "Unknown",
|
|
231
|
+
when: (/* @__PURE__ */ new Date()).toUTCString(),
|
|
232
|
+
manageUrl: `${process.env.BETTER_AUTH_URL || process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || ""}/dashboard/settings`
|
|
233
|
+
})
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error("Failed to send passkey-change notification", error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function createAuth(overrides = {}) {
|
|
240
|
+
const socialProviders = __spreadValues(__spreadValues({}, config.AUTH_ENABLE_GOOGLE ? {
|
|
241
|
+
google: {
|
|
242
|
+
clientId: process.env.AUTH_GOOGLE_ID || process.env.GOOGLE_CLIENT_ID || "",
|
|
243
|
+
clientSecret: process.env.AUTH_GOOGLE_SECRET || process.env.GOOGLE_CLIENT_SECRET || ""
|
|
244
|
+
}
|
|
245
|
+
} : {}), overrides.socialProviders);
|
|
246
|
+
const emailAndPasswordEnabled = config.AUTH_ENABLE_EMAIL_PASSWORD;
|
|
247
|
+
const hasEnabledSocialProvider = Object.values(socialProviders).some(Boolean);
|
|
248
|
+
if (!hasEnabledSocialProvider && !emailAndPasswordEnabled) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
"No auth provider is enabled \u2014 every social provider is off and emailAndPassword.enabled is false. Enable AUTH_ENABLE_GOOGLE / AUTH_ENABLE_EMAIL_PASSWORD, or pass your own provider via createAuth({ socialProviders: {...} })."
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const baseURL = process.env.NEXTAUTH_URL || process.env.BETTER_AUTH_URL || "http://localhost:3000";
|
|
254
|
+
return betterAuth({
|
|
255
|
+
database: drizzleAdapter(db, {
|
|
256
|
+
provider: "pg",
|
|
257
|
+
schema: {
|
|
258
|
+
user: users,
|
|
259
|
+
account: accounts,
|
|
260
|
+
session: sessions,
|
|
261
|
+
verification: verifications,
|
|
262
|
+
twoFactor: twoFactors,
|
|
263
|
+
passkey: passkeys
|
|
264
|
+
}
|
|
265
|
+
}),
|
|
266
|
+
// Session configuration (30 days expiration matching previous NextAuth setup)
|
|
267
|
+
session: {
|
|
268
|
+
expiresIn: 60 * 60 * 24 * 30,
|
|
269
|
+
// 30 days
|
|
270
|
+
updateAge: 60 * 60 * 24,
|
|
271
|
+
// Update session every 24 hours
|
|
272
|
+
cookieCache: {
|
|
273
|
+
enabled: true,
|
|
274
|
+
// Enable hybrid session caching for performance
|
|
275
|
+
maxAge: 5 * 60
|
|
276
|
+
// 5 minutes
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
// Email & Password Authentication (replaces Credentials provider)
|
|
280
|
+
// Toggle per-project via AUTH_ENABLE_EMAIL_PASSWORD in that project's own .env.*
|
|
281
|
+
emailAndPassword: {
|
|
282
|
+
enabled: emailAndPasswordEnabled,
|
|
283
|
+
requireEmailVerification: false,
|
|
284
|
+
// Use bcrypt to maintain compatibility with existing user passwords
|
|
285
|
+
async hash(password) {
|
|
286
|
+
return await bcrypt.hash(password, 10);
|
|
287
|
+
},
|
|
288
|
+
async verify({ hash, password }) {
|
|
289
|
+
return await bcrypt.compare(password, hash);
|
|
290
|
+
},
|
|
291
|
+
// `url` is fully composed by better-auth (baseURL + verification token +
|
|
292
|
+
// callbackURL) — see app/(auth)/forgot-password and reset-password for
|
|
293
|
+
// the pages this points at.
|
|
294
|
+
async sendResetPassword({ user, url }) {
|
|
295
|
+
await getResend().emails.send({
|
|
296
|
+
from: EMAIL_FROM,
|
|
297
|
+
to: [user.email],
|
|
298
|
+
subject: "Reset your password",
|
|
299
|
+
react: EmailTemplateResetPassword({ firstName: user.name || "there", url })
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
// Email verification. `requireEmailVerification` above stays false, so this
|
|
304
|
+
// is opt-in: users trigger it from the dashboard via
|
|
305
|
+
// authClient.sendVerificationEmail(). better-auth composes `url`
|
|
306
|
+
// (baseURL + token + callbackURL) and serves the /api/auth/verify-email
|
|
307
|
+
// callback that flips users.emailVerified on click.
|
|
308
|
+
emailVerification: {
|
|
309
|
+
autoSignInAfterVerification: true,
|
|
310
|
+
async sendVerificationEmail({ user, url }) {
|
|
311
|
+
await getResend().emails.send({
|
|
312
|
+
from: EMAIL_FROM,
|
|
313
|
+
to: [user.email],
|
|
314
|
+
subject: "Confirm your email",
|
|
315
|
+
react: EmailTemplateVerifyEmail({ firstName: user.name || "there", url })
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
// Social Providers — google is included by default; toggle per-project via
|
|
320
|
+
// AUTH_ENABLE_GOOGLE. overrides.socialProviders can still add more (or replace it).
|
|
321
|
+
socialProviders,
|
|
322
|
+
// User schema configuration
|
|
323
|
+
user: {
|
|
324
|
+
additionalFields: {
|
|
325
|
+
role: {
|
|
326
|
+
type: "string",
|
|
327
|
+
required: false,
|
|
328
|
+
defaultValue: "user",
|
|
329
|
+
input: false
|
|
330
|
+
// Prevent users from setting their own role
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
// Advanced options
|
|
335
|
+
advanced: {
|
|
336
|
+
generateId: () => __require("@paralleldrive/cuid2").createId()
|
|
337
|
+
},
|
|
338
|
+
// Every new session row corresponds to a sign-in (credential or OAuth) —
|
|
339
|
+
// use that as the "last logged in" signal rather than session updates,
|
|
340
|
+
// which also fire on cookie-cache refresh / expiry extension.
|
|
341
|
+
databaseHooks: {
|
|
342
|
+
session: {
|
|
343
|
+
create: {
|
|
344
|
+
async after(session, context) {
|
|
345
|
+
var _a, _b, _c, _d;
|
|
346
|
+
const impersonatedBy = session.impersonatedBy;
|
|
347
|
+
if (impersonatedBy) {
|
|
348
|
+
const target = await db.select({ email: users.email }).from(users).where(eq(users.id, session.userId)).limit(1);
|
|
349
|
+
await logActivity({
|
|
350
|
+
userId: impersonatedBy,
|
|
351
|
+
action: "impersonation_started",
|
|
352
|
+
description: `Admin started impersonating ${(_b = (_a = target[0]) == null ? void 0 : _a.email) != null ? _b : session.userId}`,
|
|
353
|
+
metadata: { impersonatedUserId: session.userId }
|
|
354
|
+
});
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
await db.update(users).set({ lastLoginAt: /* @__PURE__ */ new Date() }).where(eq(users.id, session.userId));
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.error("Failed to update lastLoginAt", error);
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const headers = context == null ? void 0 : context.headers;
|
|
364
|
+
const ipAddress = getClientIp(headers);
|
|
365
|
+
const userAgent = (_c = headers == null ? void 0 : headers.get("user-agent")) != null ? _c : null;
|
|
366
|
+
const referrerUrl = ((_d = context == null ? void 0 : context.getCookie) == null ? void 0 : _d.call(context, LOGIN_REFERRER_COOKIE)) || (headers == null ? void 0 : headers.get("referer")) || null;
|
|
367
|
+
const device = parseUserAgent(userAgent);
|
|
368
|
+
const geo = lookupGeoLocation(ipAddress);
|
|
369
|
+
const [{ count }] = await db.select({ count: sql`count(*)` }).from(sessions).where(eq(sessions.userId, session.userId));
|
|
370
|
+
const isFirstSession = Number(count) <= 1;
|
|
371
|
+
await logActivity({
|
|
372
|
+
userId: session.userId,
|
|
373
|
+
action: "login",
|
|
374
|
+
description: isFirstSession ? "New account registered and signed in" : "User logged in",
|
|
375
|
+
ipAddress,
|
|
376
|
+
userAgent,
|
|
377
|
+
referrerUrl,
|
|
378
|
+
os: device.os,
|
|
379
|
+
browser: device.browser,
|
|
380
|
+
deviceType: device.deviceType,
|
|
381
|
+
country: geo.country,
|
|
382
|
+
city: geo.city
|
|
383
|
+
});
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error("Failed to log login activity", error);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
// better-auth's admin plugin ends impersonation by deleting the
|
|
392
|
+
// impersonation session and restoring the admin's original session
|
|
393
|
+
// straight from a signed cookie — it never runs session.create, so
|
|
394
|
+
// there's no databaseHooks entry point to log the stop event from.
|
|
395
|
+
// Reading the session here (before the endpoint deletes it) is the only
|
|
396
|
+
// point where both the admin id and the impersonated user id are still
|
|
397
|
+
// available together.
|
|
398
|
+
hooks: {
|
|
399
|
+
before: createAuthMiddleware(async (ctx) => {
|
|
400
|
+
var _a, _b, _c, _d;
|
|
401
|
+
if (ctx.path === "/sign-in/email" || ctx.path === "/sign-up/email") {
|
|
402
|
+
const fingerprint = (_a = ctx.headers) == null ? void 0 : _a.get(DEVICE_FINGERPRINT_HEADER);
|
|
403
|
+
const ip = getClientIp(ctx.headers);
|
|
404
|
+
const throttled = fingerprint && await isDeviceThrottled(fingerprint) || ip && await isDeviceThrottled(`ip:${ip}`, MAX_IP);
|
|
405
|
+
if (throttled) {
|
|
406
|
+
throw new APIError("TOO_MANY_REQUESTS", {
|
|
407
|
+
message: "Too many attempts from this device. Try again in a few minutes."
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (ctx.path === "/sign-in/social") {
|
|
412
|
+
if (((_b = ctx.body) == null ? void 0 : _b.provider) === "google") {
|
|
413
|
+
const flags = await getEffectiveAuthFlags();
|
|
414
|
+
if (!flags.google) {
|
|
415
|
+
throw new APIError("FORBIDDEN", { message: "Google sign-in is currently disabled." });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (ctx.path === "/sign-in/email") {
|
|
421
|
+
const flags = await getEffectiveAuthFlags();
|
|
422
|
+
if (!flags.emailPassword) {
|
|
423
|
+
throw new APIError("FORBIDDEN", { message: "Email/password sign-in is currently disabled." });
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (ctx.path === "/one-tap/callback") {
|
|
428
|
+
const flags = await getEffectiveAuthFlags();
|
|
429
|
+
if (!flags.google || !flags.oneTap) {
|
|
430
|
+
throw new APIError("FORBIDDEN", { message: "Google One Tap is currently disabled." });
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (ctx.path !== "/admin/stop-impersonating") return;
|
|
435
|
+
const current = await getSessionFromCtx(ctx, { disableCookieCache: true });
|
|
436
|
+
if (current == null ? void 0 : current.session.impersonatedBy) {
|
|
437
|
+
const target = await db.select({ email: users.email }).from(users).where(eq(users.id, current.session.userId)).limit(1);
|
|
438
|
+
await logActivity({
|
|
439
|
+
userId: current.session.impersonatedBy,
|
|
440
|
+
action: "impersonation_stopped",
|
|
441
|
+
description: `Admin stopped impersonating ${(_d = (_c = target[0]) == null ? void 0 : _c.email) != null ? _d : current.session.userId}`,
|
|
442
|
+
metadata: { impersonatedUserId: current.session.userId }
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}),
|
|
446
|
+
after: createAuthMiddleware(async (ctx) => {
|
|
447
|
+
var _a, _b, _c, _d;
|
|
448
|
+
if (ctx.path === "/passkey/verify-registration" || ctx.path === "/passkey/delete-passkey") {
|
|
449
|
+
if (ctx.context.returned instanceof APIError) return;
|
|
450
|
+
await notifyPasskeyChange(
|
|
451
|
+
ctx,
|
|
452
|
+
ctx.path === "/passkey/verify-registration" ? "added" : "removed"
|
|
453
|
+
);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (ctx.path !== "/sign-in/email" && ctx.path !== "/sign-up/email") return;
|
|
457
|
+
const fingerprint = (_a = ctx.headers) == null ? void 0 : _a.get(DEVICE_FINGERPRINT_HEADER);
|
|
458
|
+
const ip = getClientIp(ctx.headers);
|
|
459
|
+
const ipKey = ip ? `ip:${ip}` : null;
|
|
460
|
+
if (!fingerprint && !ipKey) return;
|
|
461
|
+
const failed = ctx.context.returned instanceof APIError;
|
|
462
|
+
const rawEmail = (_b = ctx.body) == null ? void 0 : _b.email;
|
|
463
|
+
const meta = {
|
|
464
|
+
ipAddress: ip,
|
|
465
|
+
userAgent: (_d = (_c = ctx.headers) == null ? void 0 : _c.get("user-agent")) != null ? _d : null,
|
|
466
|
+
email: typeof rawEmail === "string" ? rawEmail.toLowerCase() : null
|
|
467
|
+
};
|
|
468
|
+
const keys = [fingerprint, ipKey].filter((k) => !!k);
|
|
469
|
+
if (ctx.path === "/sign-up/email") {
|
|
470
|
+
await Promise.all(keys.map((key) => recordDeviceAttempt(key, __spreadProps(__spreadValues({}, meta), { kind: "signup" }))));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (failed) {
|
|
474
|
+
await Promise.all(keys.map((key) => recordDeviceAttempt(key, __spreadProps(__spreadValues({}, meta), { kind: "signin" }))));
|
|
475
|
+
await logFailedLogin(ctx);
|
|
476
|
+
} else {
|
|
477
|
+
await Promise.all(keys.map((key) => clearDeviceThrottle(key)));
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
},
|
|
481
|
+
// Base URL for callbacks
|
|
482
|
+
baseURL,
|
|
483
|
+
// Secret for signing cookies and tokens
|
|
484
|
+
secret: process.env.AUTH_SECRET,
|
|
485
|
+
// The OAuth `state` is a one-time verification row: the callback consumes
|
|
486
|
+
// and deletes it. When the same callback URL gets hit twice (browser
|
|
487
|
+
// prefetch / bfcache / double nav — common on a repeat Google sign-in
|
|
488
|
+
// where Google auto-redirects with no consent screen), the first hit
|
|
489
|
+
// logs the user in and the second finds no `state` row and throws
|
|
490
|
+
// `please_restart_the_process`. Default sends that to better-auth's bare
|
|
491
|
+
// error page; point it at /login instead, which already bounces an
|
|
492
|
+
// authenticated user straight to their callbackUrl — so the losing race
|
|
493
|
+
// just lands them logged in. See better-auth#5658 / #6544.
|
|
494
|
+
onAPIError: {
|
|
495
|
+
errorURL: "/login"
|
|
496
|
+
},
|
|
497
|
+
plugins: [
|
|
498
|
+
// checks to see if oneTap is enabled and if google provider is available, then add the oneTap plugin
|
|
499
|
+
...config.AUTH_ENABLE_ONE_TAP && Boolean(socialProviders.google) ? [oneTap()] : [],
|
|
500
|
+
// Registered for impersonation (auth.api.impersonateUser / stopImpersonating)
|
|
501
|
+
// only — role/ban/delete stay on our own custom routes above. adminRoles
|
|
502
|
+
// matches our existing role column so the plugin's own permission checks
|
|
503
|
+
// line up with hasRole(role, 'admin'). banReason is remapped onto our
|
|
504
|
+
// existing bannedReason column instead of adding a duplicate field.
|
|
505
|
+
admin({
|
|
506
|
+
adminRoles: ["admin"],
|
|
507
|
+
schema: {
|
|
508
|
+
user: {
|
|
509
|
+
fields: {
|
|
510
|
+
banReason: "bannedReason"
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}),
|
|
515
|
+
// TOTP second factor + encrypted backup codes. `issuer` is the label
|
|
516
|
+
// shown in the user's authenticator app.
|
|
517
|
+
twoFactor({ issuer: config.APP_NAME }),
|
|
518
|
+
// WebAuthn passkeys (Touch ID / Windows Hello / security keys) as a
|
|
519
|
+
// passwordless sign-in method. rpID must be the bare host and origin
|
|
520
|
+
// the full URL — both derived from baseURL above.
|
|
521
|
+
passkey({
|
|
522
|
+
rpID: new URL(baseURL).hostname,
|
|
523
|
+
rpName: config.APP_NAME,
|
|
524
|
+
origin: baseURL
|
|
525
|
+
})
|
|
526
|
+
]
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
var _auth;
|
|
530
|
+
function getDefaultAuth() {
|
|
531
|
+
if (!_auth) _auth = createAuth();
|
|
532
|
+
return _auth;
|
|
533
|
+
}
|
|
534
|
+
var auth = new Proxy({}, {
|
|
535
|
+
get(_target, prop) {
|
|
536
|
+
const instance = getDefaultAuth();
|
|
537
|
+
const value = Reflect.get(instance, prop);
|
|
538
|
+
return typeof value === "function" ? value.bind(instance) : value;
|
|
539
|
+
},
|
|
540
|
+
// "prop" in auth (e.g. better-auth's toNextJsHandler does `"handler" in auth`)
|
|
541
|
+
// must also see the real instance — without this it checks the empty
|
|
542
|
+
// placeholder target and always reports false.
|
|
543
|
+
has(_target, prop) {
|
|
544
|
+
return Reflect.has(getDefaultAuth(), prop);
|
|
545
|
+
},
|
|
546
|
+
ownKeys(_target) {
|
|
547
|
+
return Reflect.ownKeys(getDefaultAuth());
|
|
548
|
+
},
|
|
549
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
550
|
+
return Reflect.getOwnPropertyDescriptor(getDefaultAuth(), prop);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
export { auth, createAuth };
|
|
555
|
+
//# sourceMappingURL=chunk-2FW7C3KS.mjs.map
|
|
556
|
+
//# sourceMappingURL=chunk-2FW7C3KS.mjs.map
|