@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.
Files changed (199) hide show
  1. package/README.md +339 -0
  2. package/components/auth/authCard.tsx +92 -0
  3. package/components/auth/authShell.tsx +46 -0
  4. package/components/auth/deviceCheck.tsx +52 -0
  5. package/components/auth/emailPasswordLogin.tsx +136 -0
  6. package/components/auth/emailPasswordSignup.tsx +178 -0
  7. package/components/auth/forgotPasswordForm.tsx +66 -0
  8. package/components/auth/loginErrorRecovery.tsx +117 -0
  9. package/components/auth/logoutButtons.tsx +26 -0
  10. package/components/auth/oneTap.tsx +50 -0
  11. package/components/auth/passkeyLogin.tsx +50 -0
  12. package/components/auth/resetPasswordForm.tsx +112 -0
  13. package/components/auth/socialLogin.tsx +51 -0
  14. package/components/auth/twoFactorVerifyForm.tsx +100 -0
  15. package/components/dashboard/accountDetailsCard.tsx +58 -0
  16. package/components/dashboard/activityLogsCard.tsx +141 -0
  17. package/components/dashboard/activityLogsCardSkeleton.tsx +20 -0
  18. package/components/dashboard/activityLogsSection.tsx +16 -0
  19. package/components/dashboard/admin/activityLogsSection.tsx +13 -0
  20. package/components/dashboard/admin/adminActivityLogsSkeleton.tsx +34 -0
  21. package/components/dashboard/admin/adminActivityLogsTable.tsx +319 -0
  22. package/components/dashboard/admin/adminStatsGrid.tsx +30 -0
  23. package/components/dashboard/admin/adminStatsGridSkeleton.tsx +16 -0
  24. package/components/dashboard/admin/adminUserActivityLogCard.tsx +53 -0
  25. package/components/dashboard/admin/adminUserActivityLogTable.tsx +69 -0
  26. package/components/dashboard/admin/adminUserLoginInfoCard.tsx +50 -0
  27. package/components/dashboard/admin/adminUserLoginInfoTable.tsx +63 -0
  28. package/components/dashboard/admin/adminUserSessionsCard.tsx +225 -0
  29. package/components/dashboard/admin/adminUserStatusCard.tsx +57 -0
  30. package/components/dashboard/admin/adminUsersSkeleton.tsx +30 -0
  31. package/components/dashboard/admin/adminUsersTable.tsx +596 -0
  32. package/components/dashboard/admin/deviceThrottleCard.tsx +59 -0
  33. package/components/dashboard/admin/deviceThrottleTable.tsx +93 -0
  34. package/components/dashboard/admin/impersonationLogCard.tsx +73 -0
  35. package/components/dashboard/admin/newDeviceCard.tsx +35 -0
  36. package/components/dashboard/admin/newDeviceTable.tsx +71 -0
  37. package/components/dashboard/admin/revokeAllSessionsButton.tsx +77 -0
  38. package/components/dashboard/admin/sharedDeviceCard.tsx +42 -0
  39. package/components/dashboard/admin/sharedDeviceTable.tsx +75 -0
  40. package/components/dashboard/admin/systemSettingsForm.tsx +200 -0
  41. package/components/dashboard/admin/systemSettingsSection.tsx +9 -0
  42. package/components/dashboard/admin/systemSettingsSkeleton.tsx +29 -0
  43. package/components/dashboard/admin/usersSection.tsx +13 -0
  44. package/components/dashboard/adminSection.tsx +31 -0
  45. package/components/dashboard/appSidebar.tsx +170 -0
  46. package/components/dashboard/dashboardHeader.tsx +85 -0
  47. package/components/dashboard/heartbeat.tsx +22 -0
  48. package/components/dashboard/impersonationBanner.tsx +50 -0
  49. package/components/dashboard/passkeysCard.tsx +187 -0
  50. package/components/dashboard/profileCard.tsx +42 -0
  51. package/components/dashboard/sectionErrorFallback.tsx +18 -0
  52. package/components/dashboard/sessionsCard.tsx +194 -0
  53. package/components/dashboard/sessionsCardSkeleton.tsx +20 -0
  54. package/components/dashboard/sessionsSection.tsx +22 -0
  55. package/components/dashboard/twoFactorCard.tsx +344 -0
  56. package/components/dashboard/verifyEmailButton.tsx +32 -0
  57. package/components/email/email-template-2.tsx +44 -0
  58. package/components/email/email-template-new-device.tsx +80 -0
  59. package/components/email/email-template-passkey-change.tsx +81 -0
  60. package/components/email/email-template-reset-password.tsx +56 -0
  61. package/components/email/email-template-verify-email.tsx +55 -0
  62. package/components/email/email-template-welcome.tsx +53 -0
  63. package/components/email/email-template.tsx +33 -0
  64. package/components/email/send-email-button.tsx +61 -0
  65. package/components/providers/theme-provider.tsx +9 -0
  66. package/components/theme/theme-toggle.tsx +53 -0
  67. package/components/ui/alert-dialog.tsx +141 -0
  68. package/components/ui/avatar.tsx +53 -0
  69. package/components/ui/badge.tsx +46 -0
  70. package/components/ui/button.tsx +64 -0
  71. package/components/ui/card.tsx +92 -0
  72. package/components/ui/checkbox.tsx +32 -0
  73. package/components/ui/data-table.tsx +302 -0
  74. package/components/ui/dialog.tsx +143 -0
  75. package/components/ui/dropdown-menu.tsx +203 -0
  76. package/components/ui/input.tsx +21 -0
  77. package/components/ui/label.tsx +26 -0
  78. package/components/ui/password-input.tsx +38 -0
  79. package/components/ui/separator.tsx +28 -0
  80. package/components/ui/sheet.tsx +143 -0
  81. package/components/ui/sidebar.tsx +726 -0
  82. package/components/ui/skeleton.tsx +13 -0
  83. package/components/ui/switch.tsx +35 -0
  84. package/components/ui/table.tsx +116 -0
  85. package/components/ui/textarea.tsx +18 -0
  86. package/components/ui/tooltip.tsx +57 -0
  87. package/dist/activity/logger.d.mts +18 -0
  88. package/dist/activity/logger.d.ts +18 -0
  89. package/dist/activity/logger.js +289 -0
  90. package/dist/activity/logger.js.map +1 -0
  91. package/dist/activity/logger.mjs +7 -0
  92. package/dist/activity/logger.mjs.map +1 -0
  93. package/dist/activity/queries.d.mts +98 -0
  94. package/dist/activity/queries.d.ts +98 -0
  95. package/dist/activity/queries.js +372 -0
  96. package/dist/activity/queries.js.map +1 -0
  97. package/dist/activity/queries.mjs +124 -0
  98. package/dist/activity/queries.mjs.map +1 -0
  99. package/dist/admin/queries.d.mts +68 -0
  100. package/dist/admin/queries.d.ts +68 -0
  101. package/dist/admin/queries.js +450 -0
  102. package/dist/admin/queries.js.map +1 -0
  103. package/dist/admin/queries.mjs +143 -0
  104. package/dist/admin/queries.mjs.map +1 -0
  105. package/dist/auth/helpers.d.mts +111 -0
  106. package/dist/auth/helpers.d.ts +111 -0
  107. package/dist/auth/helpers.js +1023 -0
  108. package/dist/auth/helpers.js.map +1 -0
  109. package/dist/auth/helpers.mjs +67 -0
  110. package/dist/auth/helpers.mjs.map +1 -0
  111. package/dist/auth/index.d.mts +6589 -0
  112. package/dist/auth/index.d.ts +6589 -0
  113. package/dist/auth/index.js +968 -0
  114. package/dist/auth/index.js.map +1 -0
  115. package/dist/auth/index.mjs +10 -0
  116. package/dist/auth/index.mjs.map +1 -0
  117. package/dist/auth-client/index.d.mts +4864 -0
  118. package/dist/auth-client/index.d.ts +4864 -0
  119. package/dist/auth-client/index.js +55 -0
  120. package/dist/auth-client/index.js.map +1 -0
  121. package/dist/auth-client/index.mjs +49 -0
  122. package/dist/auth-client/index.mjs.map +1 -0
  123. package/dist/chunk-2FW7C3KS.mjs +556 -0
  124. package/dist/chunk-2FW7C3KS.mjs.map +1 -0
  125. package/dist/chunk-ENGYO4K7.mjs +189 -0
  126. package/dist/chunk-ENGYO4K7.mjs.map +1 -0
  127. package/dist/chunk-GTPTVNY2.mjs +40 -0
  128. package/dist/chunk-GTPTVNY2.mjs.map +1 -0
  129. package/dist/chunk-HYKUB3A2.mjs +30 -0
  130. package/dist/chunk-HYKUB3A2.mjs.map +1 -0
  131. package/dist/chunk-JIYHBWAU.mjs +79 -0
  132. package/dist/chunk-JIYHBWAU.mjs.map +1 -0
  133. package/dist/chunk-K6OJCSJS.mjs +41 -0
  134. package/dist/chunk-K6OJCSJS.mjs.map +1 -0
  135. package/dist/chunk-N6ECRCHY.mjs +61 -0
  136. package/dist/chunk-N6ECRCHY.mjs.map +1 -0
  137. package/dist/chunk-YN4436PK.mjs +33 -0
  138. package/dist/chunk-YN4436PK.mjs.map +1 -0
  139. package/dist/config/env.d.mts +15 -0
  140. package/dist/config/env.d.ts +15 -0
  141. package/dist/config/env.js +42 -0
  142. package/dist/config/env.js.map +1 -0
  143. package/dist/config/env.mjs +4 -0
  144. package/dist/config/env.mjs.map +1 -0
  145. package/dist/db/index.d.mts +10 -0
  146. package/dist/db/index.d.ts +10 -0
  147. package/dist/db/index.js +252 -0
  148. package/dist/db/index.js.map +1 -0
  149. package/dist/db/index.mjs +6 -0
  150. package/dist/db/index.mjs.map +1 -0
  151. package/dist/db/schema.d.mts +2 -0
  152. package/dist/db/schema.d.ts +2 -0
  153. package/dist/db/schema.js +186 -0
  154. package/dist/db/schema.js.map +1 -0
  155. package/dist/db/schema.mjs +4 -0
  156. package/dist/db/schema.mjs.map +1 -0
  157. package/dist/schema-CgkDv4wJ.d.mts +1758 -0
  158. package/dist/schema-CgkDv4wJ.d.ts +1758 -0
  159. package/dist/sessions/queries.d.mts +34 -0
  160. package/dist/sessions/queries.d.ts +34 -0
  161. package/dist/sessions/queries.js +268 -0
  162. package/dist/sessions/queries.js.map +1 -0
  163. package/dist/sessions/queries.mjs +23 -0
  164. package/dist/sessions/queries.mjs.map +1 -0
  165. package/dist/settings/queries.d.mts +20 -0
  166. package/dist/settings/queries.d.ts +20 -0
  167. package/dist/settings/queries.js +344 -0
  168. package/dist/settings/queries.js.map +1 -0
  169. package/dist/settings/queries.mjs +7 -0
  170. package/dist/settings/queries.mjs.map +1 -0
  171. package/dist/types/auth/loginSchema.d.mts +8 -0
  172. package/dist/types/auth/loginSchema.d.ts +8 -0
  173. package/dist/types/auth/loginSchema.js +18 -0
  174. package/dist/types/auth/loginSchema.js.map +1 -0
  175. package/dist/types/auth/loginSchema.mjs +16 -0
  176. package/dist/types/auth/loginSchema.mjs.map +1 -0
  177. package/dist/types/auth/signupSchema.d.mts +9 -0
  178. package/dist/types/auth/signupSchema.d.ts +9 -0
  179. package/dist/types/auth/signupSchema.js +21 -0
  180. package/dist/types/auth/signupSchema.js.map +1 -0
  181. package/dist/types/auth/signupSchema.mjs +19 -0
  182. package/dist/types/auth/signupSchema.mjs.map +1 -0
  183. package/dist/users/queries.d.mts +71 -0
  184. package/dist/users/queries.d.ts +71 -0
  185. package/dist/users/queries.js +293 -0
  186. package/dist/users/queries.js.map +1 -0
  187. package/dist/users/queries.mjs +47 -0
  188. package/dist/users/queries.mjs.map +1 -0
  189. package/lib/auth-client.ts +58 -0
  190. package/lib/cookie-names.ts +13 -0
  191. package/lib/device-fingerprint.ts +21 -0
  192. package/lib/formatters.ts +53 -0
  193. package/lib/recalculate-columns.ts +75 -0
  194. package/lib/request-info.ts +78 -0
  195. package/lib/utils.ts +6 -0
  196. package/package.json +301 -0
  197. package/styles/theme.css +124 -0
  198. package/types/auth/loginSchema.ts +11 -0
  199. 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