@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,71 @@
1
+ declare function getUserById(id: string): Promise<{
2
+ id: string;
3
+ name: string;
4
+ email: string;
5
+ password: string | null;
6
+ emailVerified: boolean;
7
+ image: string | null;
8
+ role: "user" | "admin";
9
+ banned: boolean;
10
+ bannedAt: Date | null;
11
+ bannedReason: string | null;
12
+ banExpires: Date | null;
13
+ lastLoginAt: Date | null;
14
+ lastActiveAt: Date | null;
15
+ twoFactorEnabled: boolean;
16
+ createdAt: Date;
17
+ updatedAt: Date;
18
+ }>;
19
+ declare function getAdminEmails(): Promise<string[]>;
20
+ interface UserFilters {
21
+ search?: string;
22
+ page?: number;
23
+ limit?: number;
24
+ }
25
+ declare function getUsersFiltered({ search, page, limit }: UserFilters): Promise<{
26
+ users: {
27
+ id: string;
28
+ name: string;
29
+ email: string;
30
+ password: string | null;
31
+ emailVerified: boolean;
32
+ image: string | null;
33
+ role: "user" | "admin";
34
+ banned: boolean;
35
+ bannedAt: Date | null;
36
+ bannedReason: string | null;
37
+ banExpires: Date | null;
38
+ lastLoginAt: Date | null;
39
+ lastActiveAt: Date | null;
40
+ twoFactorEnabled: boolean;
41
+ createdAt: Date;
42
+ updatedAt: Date;
43
+ }[];
44
+ total: number;
45
+ page: number;
46
+ limit: number;
47
+ pages: number;
48
+ }>;
49
+ interface UserExportFilters {
50
+ search?: string;
51
+ }
52
+ declare function getUsersForExport({ search }: UserExportFilters): Promise<{
53
+ id: string;
54
+ name: string;
55
+ email: string;
56
+ password: string | null;
57
+ emailVerified: boolean;
58
+ image: string | null;
59
+ role: "user" | "admin";
60
+ banned: boolean;
61
+ bannedAt: Date | null;
62
+ bannedReason: string | null;
63
+ banExpires: Date | null;
64
+ lastLoginAt: Date | null;
65
+ lastActiveAt: Date | null;
66
+ twoFactorEnabled: boolean;
67
+ createdAt: Date;
68
+ updatedAt: Date;
69
+ }[]>;
70
+
71
+ export { type UserExportFilters, type UserFilters, getAdminEmails, getUserById, getUsersFiltered, getUsersForExport };
@@ -0,0 +1,293 @@
1
+ 'use strict';
2
+
3
+ require('server-only');
4
+ var serverless = require('@neondatabase/serverless');
5
+ var neonHttp = require('drizzle-orm/neon-http');
6
+ var nodePostgres = require('drizzle-orm/node-postgres');
7
+ var pg = require('pg');
8
+ var pgCore = require('drizzle-orm/pg-core');
9
+ var drizzleOrm = require('drizzle-orm');
10
+ var cuid2 = require('@paralleldrive/cuid2');
11
+ var zod = require('zod');
12
+
13
+ var __defProp = Object.defineProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+
19
+ // server/db/schema.ts
20
+ var schema_exports = {};
21
+ __export(schema_exports, {
22
+ ActivityActionEnum: () => ActivityActionEnum,
23
+ RoleEnum: () => RoleEnum,
24
+ accounts: () => accounts,
25
+ activityLogs: () => activityLogs,
26
+ authThrottle: () => authThrottle,
27
+ deviceFingerprints: () => deviceFingerprints,
28
+ passkeys: () => passkeys,
29
+ sessions: () => sessions,
30
+ systemSettings: () => systemSettings,
31
+ twoFactors: () => twoFactors,
32
+ users: () => users,
33
+ verifications: () => verifications
34
+ });
35
+ var RoleEnum = pgCore.pgEnum("roles", ["user", "admin"]);
36
+ var ActivityActionEnum = pgCore.pgEnum("activity_actions", [
37
+ "login",
38
+ "logout",
39
+ "login_failed",
40
+ "password_changed",
41
+ "email_changed",
42
+ "profile_updated",
43
+ "session_revoked",
44
+ "user_deleted",
45
+ "user_banned",
46
+ "user_unbanned",
47
+ "role_changed",
48
+ "impersonation_started",
49
+ "impersonation_stopped",
50
+ "settings_changed",
51
+ "passkey_added",
52
+ "passkey_removed"
53
+ ]);
54
+ var users = pgCore.pgTable("user", {
55
+ id: pgCore.text("id").notNull().primaryKey().$defaultFn(() => cuid2.createId()),
56
+ name: pgCore.text("name").notNull(),
57
+ email: pgCore.text("email").notNull().unique(),
58
+ password: pgCore.text("password"),
59
+ emailVerified: pgCore.boolean("emailVerified").notNull().default(false),
60
+ image: pgCore.text("image"),
61
+ role: RoleEnum("roles").default("user").notNull(),
62
+ banned: pgCore.boolean("banned").default(false).notNull(),
63
+ bannedAt: pgCore.timestamp("bannedAt"),
64
+ bannedReason: pgCore.text("bannedReason"),
65
+ // Required by better-auth's admin plugin schema (auto-expiring bans). We
66
+ // never set this ourselves, so it stays null and bans behave as permanent,
67
+ // same as before the plugin was added.
68
+ banExpires: pgCore.timestamp("banExpires"),
69
+ lastLoginAt: pgCore.timestamp("lastLoginAt"),
70
+ lastActiveAt: pgCore.timestamp("lastActiveAt"),
71
+ // Set by better-auth's twoFactor plugin — true once the user has
72
+ // completed TOTP setup (see twoFactor pgTable below for the secret /
73
+ // backup codes themselves).
74
+ twoFactorEnabled: pgCore.boolean("twoFactorEnabled").notNull().default(false),
75
+ createdAt: pgCore.timestamp("createdAt").notNull().defaultNow(),
76
+ updatedAt: pgCore.timestamp("updatedAt").notNull().defaultNow()
77
+ });
78
+ var sessions = pgCore.pgTable("session", {
79
+ id: pgCore.text("id").notNull().primaryKey().$defaultFn(() => cuid2.createId()),
80
+ expiresAt: pgCore.timestamp("expiresAt").notNull(),
81
+ token: pgCore.text("token").notNull().unique(),
82
+ createdAt: pgCore.timestamp("createdAt").notNull().defaultNow(),
83
+ updatedAt: pgCore.timestamp("updatedAt").notNull().defaultNow(),
84
+ ipAddress: pgCore.text("ipAddress"),
85
+ userAgent: pgCore.text("userAgent"),
86
+ userId: pgCore.text("userId").notNull().references(() => users.id, { onDelete: "cascade" }),
87
+ // Set by better-auth's admin plugin while this session is an admin
88
+ // impersonating another user; holds the impersonating admin's user id.
89
+ impersonatedBy: pgCore.text("impersonatedBy")
90
+ });
91
+ var accounts = pgCore.pgTable("account", {
92
+ id: pgCore.text("id").notNull().primaryKey().$defaultFn(() => cuid2.createId()),
93
+ accountId: pgCore.text("accountId").notNull(),
94
+ providerId: pgCore.text("providerId").notNull(),
95
+ userId: pgCore.text("userId").notNull().references(() => users.id, { onDelete: "cascade" }),
96
+ accessToken: pgCore.text("accessToken"),
97
+ refreshToken: pgCore.text("refreshToken"),
98
+ idToken: pgCore.text("idToken"),
99
+ accessTokenExpiresAt: pgCore.timestamp("accessTokenExpiresAt"),
100
+ refreshTokenExpiresAt: pgCore.timestamp("refreshTokenExpiresAt"),
101
+ scope: pgCore.text("scope"),
102
+ password: pgCore.text("password"),
103
+ createdAt: pgCore.timestamp("createdAt").notNull().defaultNow(),
104
+ updatedAt: pgCore.timestamp("updatedAt").notNull().defaultNow()
105
+ });
106
+ var twoFactors = pgCore.pgTable("twoFactor", {
107
+ id: pgCore.text("id").notNull().primaryKey().$defaultFn(() => cuid2.createId()),
108
+ secret: pgCore.text("secret").notNull(),
109
+ backupCodes: pgCore.text("backupCodes").notNull(),
110
+ userId: pgCore.text("userId").notNull().references(() => users.id, { onDelete: "cascade" })
111
+ });
112
+ var passkeys = pgCore.pgTable("passkey", {
113
+ id: pgCore.text("id").notNull().primaryKey().$defaultFn(() => cuid2.createId()),
114
+ name: pgCore.text("name"),
115
+ publicKey: pgCore.text("publicKey").notNull(),
116
+ userId: pgCore.text("userId").notNull().references(() => users.id, { onDelete: "cascade" }),
117
+ credentialID: pgCore.text("credentialID").notNull().unique(),
118
+ counter: pgCore.integer("counter").notNull(),
119
+ deviceType: pgCore.text("deviceType").notNull(),
120
+ backedUp: pgCore.boolean("backedUp").notNull(),
121
+ transports: pgCore.text("transports"),
122
+ createdAt: pgCore.timestamp("createdAt"),
123
+ aaguid: pgCore.text("aaguid")
124
+ });
125
+ var verifications = pgCore.pgTable("verification", {
126
+ id: pgCore.text("id").notNull().primaryKey().$defaultFn(() => cuid2.createId()),
127
+ identifier: pgCore.text("identifier").notNull(),
128
+ value: pgCore.text("value").notNull(),
129
+ expiresAt: pgCore.timestamp("expiresAt").notNull(),
130
+ createdAt: pgCore.timestamp("createdAt").notNull().defaultNow(),
131
+ updatedAt: pgCore.timestamp("updatedAt").notNull().defaultNow()
132
+ });
133
+ var activityLogs = pgCore.pgTable("activity_log", {
134
+ id: pgCore.text("id").notNull().primaryKey().$defaultFn(() => cuid2.createId()),
135
+ userId: pgCore.text("userId").notNull().references(() => users.id, { onDelete: "cascade" }),
136
+ action: ActivityActionEnum("action").notNull(),
137
+ description: pgCore.text("description").notNull(),
138
+ ipAddress: pgCore.text("ipAddress"),
139
+ userAgent: pgCore.text("userAgent"),
140
+ metadata: pgCore.text("metadata"),
141
+ // Populated on 'login' events only (registration + sign-in) — parsed from
142
+ // userAgent/ipAddress at write time so the record stays stable even if
143
+ // the parsing library or geoip database changes later.
144
+ referrerUrl: pgCore.text("referrerUrl"),
145
+ os: pgCore.text("os"),
146
+ browser: pgCore.text("browser"),
147
+ deviceType: pgCore.text("deviceType"),
148
+ country: pgCore.text("country"),
149
+ city: pgCore.text("city"),
150
+ createdAt: pgCore.timestamp("createdAt").notNull().defaultNow()
151
+ });
152
+ var deviceFingerprints = pgCore.pgTable("device_fingerprint", {
153
+ id: pgCore.text("id").notNull().primaryKey().$defaultFn(() => cuid2.createId()),
154
+ userId: pgCore.text("userId").notNull().references(() => users.id, { onDelete: "cascade" }),
155
+ visitorId: pgCore.text("visitorId").notNull(),
156
+ // Context captured at first-seen time — for the admin panel's "New devices"
157
+ // card and the alert email. Not part of the new-vs-known decision (that's
158
+ // visitorId only), just so an admin can eyeball whether a sign-in looks like
159
+ // a hijack. Nullable: an old row predates these columns, or geoip missed.
160
+ ipAddress: pgCore.text("ipAddress"),
161
+ userAgent: pgCore.text("userAgent"),
162
+ country: pgCore.text("country"),
163
+ city: pgCore.text("city"),
164
+ createdAt: pgCore.timestamp("createdAt").notNull().defaultNow()
165
+ }, (table) => [
166
+ pgCore.unique("device_fingerprint_user_visitor").on(table.userId, table.visitorId)
167
+ ]);
168
+ var authThrottle = pgCore.pgTable("auth_throttle", {
169
+ fingerprint: pgCore.text("fingerprint").notNull().primaryKey(),
170
+ count: pgCore.integer("count").notNull().default(0),
171
+ windowStart: pgCore.timestamp("windowStart").notNull().defaultNow(),
172
+ // Blockable data points, overwritten on every attempt (last-write-wins) so
173
+ // the admin panel can show who to block — by IP or by fingerprint — with no
174
+ // join and no per-attempt history table. windowStart anchors the counter's
175
+ // window; lastAttemptAt is just "most recently seen".
176
+ lastAttemptAt: pgCore.timestamp("lastAttemptAt").notNull().defaultNow(),
177
+ ipAddress: pgCore.text("ipAddress"),
178
+ userAgent: pgCore.text("userAgent"),
179
+ lastEmail: pgCore.text("lastEmail"),
180
+ lastKind: pgCore.text("lastKind")
181
+ // 'signin' | 'signup'
182
+ });
183
+ var systemSettings = pgCore.pgTable("system_settings", {
184
+ id: pgCore.text("id").primaryKey().default("default"),
185
+ maintenanceMode: pgCore.boolean("maintenanceMode").notNull().default(false),
186
+ maintenanceMessage: pgCore.text("maintenanceMessage"),
187
+ authEnableGoogle: pgCore.boolean("authEnableGoogle").notNull().default(true),
188
+ authEnableEmailPassword: pgCore.boolean("authEnableEmailPassword").notNull().default(true),
189
+ authEnableOneTap: pgCore.boolean("authEnableOneTap").notNull().default(false),
190
+ enableSessionRevocation: pgCore.boolean("enableSessionRevocation").notNull().default(true),
191
+ updatedAt: pgCore.timestamp("updatedAt").notNull().defaultNow(),
192
+ // Who last changed a setting. This row doesn't "belong" to that admin the
193
+ // way sessions/activityLogs belong to their user, so deleting that admin's
194
+ // account should null this out rather than cascade-delete the singleton row.
195
+ updatedBy: pgCore.text("updatedBy").references(() => users.id, { onDelete: "set null" })
196
+ }, (table) => [
197
+ pgCore.check("system_settings_singleton", drizzleOrm.sql`${table.id} = 'default'`)
198
+ ]);
199
+ var envSchema = zod.z.object({
200
+ NODE_ENV: zod.z.enum(["development", "production", "test"]).default("development"),
201
+ PORT: zod.z.string().default("3000").transform(Number),
202
+ DATABASE_URL: zod.z.string().default(""),
203
+ RESEND_API_KEY: zod.z.string().default(""),
204
+ // Display name for this deployment — shown as the authenticator-app label
205
+ // (2FA issuer) and the passkey relying-party name. Consuming projects set
206
+ // this in their own .env; defaults to the template name.
207
+ APP_NAME: zod.z.string().default("Next.js Template"),
208
+ // Seed-only: the live value lives in the system_settings DB row (see
209
+ // lib/settings-queries.ts) once it's been seeded on first read, and is
210
+ // changed from the admin System Settings page, not by editing this var.
211
+ ENABLE_SESSION_REVOCATION: zod.z.string().default("true").transform((val) => val === "true"),
212
+ AUTH_ENABLE_GOOGLE: zod.z.string().default("true").transform((val) => val === "true"),
213
+ AUTH_ENABLE_EMAIL_PASSWORD: zod.z.string().default("true").transform((val) => val === "true"),
214
+ AUTH_ENABLE_ONE_TAP: zod.z.string().default("false").transform((val) => val === "true"),
215
+ DB_DRIVER: zod.z.enum(["pg", "neon"]).default("pg"),
216
+ // Only relevant when DB_DRIVER=pg — whether the target Postgres server has SSL
217
+ // enabled. Self-hosted Postgres (e.g. via Docker/Dokploy) typically does not.
218
+ DATABASE_SSL: zod.z.string().default("false").transform((val) => val === "true")
219
+ });
220
+
221
+ // config/env.ts
222
+ var results = envSchema.safeParse(process.env);
223
+ if (results.success && results.data.NODE_ENV === "development") {
224
+ console.log("Environment variables loaded:", Object.keys(results.data));
225
+ }
226
+ if (!results.success) {
227
+ console.error("Invalid environment variables:", results.error.format());
228
+ throw new Error("Invalid environment variables");
229
+ }
230
+ var config = results.data;
231
+
232
+ // server/db/index.ts
233
+ var db;
234
+ if (config.DB_DRIVER === "pg") {
235
+ const pool = new pg.Pool({
236
+ connectionString: config.DATABASE_URL,
237
+ ssl: config.DATABASE_SSL ? { rejectUnauthorized: false } : void 0
238
+ });
239
+ db = nodePostgres.drizzle(pool, {
240
+ schema: schema_exports,
241
+ logger: true
242
+ });
243
+ console.log("\u{1F7E1} Using PostgreSQL (pg) driver");
244
+ } else if (config.DB_DRIVER === "neon") {
245
+ const sql3 = serverless.neon(config.DATABASE_URL);
246
+ db = neonHttp.drizzle(sql3, { schema: schema_exports, logger: true });
247
+ console.log("\u{1F7E2} Using Neon (HTTP) driver");
248
+ } else {
249
+ console.log("Database connection not established, unknown DB_DRIVER:", config.DB_DRIVER);
250
+ }
251
+ async function getUserById(id) {
252
+ const result = await db.select().from(users).where(drizzleOrm.eq(users.id, id)).limit(1);
253
+ return result[0] || null;
254
+ }
255
+ async function getAdminEmails() {
256
+ const rows = await db.select({ email: users.email }).from(users).where(drizzleOrm.eq(users.role, "admin"));
257
+ return rows.map((r) => r.email);
258
+ }
259
+ function buildUsersWhere(search) {
260
+ return search.trim() ? drizzleOrm.or(
261
+ drizzleOrm.like(users.name, `%${search}%`),
262
+ drizzleOrm.like(users.email, `%${search}%`)
263
+ ) : void 0;
264
+ }
265
+ async function getUsersFiltered({ search = "", page = 1, limit = 10 }) {
266
+ var _a, _b;
267
+ const offset = (page - 1) * limit;
268
+ const where = buildUsersWhere(search);
269
+ const [usersList, countResult] = await Promise.all([
270
+ db.select().from(users).where(where).orderBy(drizzleOrm.desc(users.createdAt)).limit(limit).offset(offset),
271
+ db.select({ count: drizzleOrm.sql`count(*)` }).from(users).where(where)
272
+ ]);
273
+ const total = Number((_b = (_a = countResult[0]) == null ? void 0 : _a.count) != null ? _b : 0);
274
+ return {
275
+ users: usersList,
276
+ total,
277
+ page,
278
+ limit,
279
+ pages: Math.ceil(total / limit)
280
+ };
281
+ }
282
+ var EXPORT_ROW_LIMIT = 1e4;
283
+ async function getUsersForExport({ search = "" }) {
284
+ const where = buildUsersWhere(search);
285
+ return db.select().from(users).where(where).orderBy(drizzleOrm.desc(users.createdAt)).limit(EXPORT_ROW_LIMIT);
286
+ }
287
+
288
+ exports.getAdminEmails = getAdminEmails;
289
+ exports.getUserById = getUserById;
290
+ exports.getUsersFiltered = getUsersFiltered;
291
+ exports.getUsersForExport = getUsersForExport;
292
+ //# sourceMappingURL=queries.js.map
293
+ //# sourceMappingURL=queries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../server/db/schema.ts","../../config/env-schema.ts","../../config/env.ts","../../server/db/index.ts","../../lib/user-queries.ts"],"names":["pgEnum","pgTable","text","createId","boolean","timestamp","integer","unique","check","sql","z","Pool","drizzlePg","neon","drizzle","eq","or","like","desc"],"mappings":"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","file":"queries.js","sourcesContent":["import { boolean, check, integer, pgEnum, pgTable, text, timestamp, unique } from \"drizzle-orm/pg-core\";\nimport { sql } from \"drizzle-orm\";\nimport { createId } from '@paralleldrive/cuid2'\n\nexport const RoleEnum = pgEnum('roles', ['user', 'admin'])\n\nexport const ActivityActionEnum = pgEnum('activity_actions', [\n 'login',\n 'logout',\n 'login_failed',\n 'password_changed',\n 'email_changed',\n 'profile_updated',\n 'session_revoked',\n 'user_deleted',\n 'user_banned',\n 'user_unbanned',\n 'role_changed',\n 'impersonation_started',\n 'impersonation_stopped',\n 'settings_changed',\n 'passkey_added',\n 'passkey_removed',\n])\n\nexport const users = pgTable(\"user\", {\n id: text(\"id\").notNull().primaryKey().$defaultFn(() => createId()),\n name: text('name').notNull(),\n email: text('email').notNull().unique(),\n password: text('password'),\n emailVerified: boolean(\"emailVerified\").notNull().default(false),\n image: text(\"image\"),\n role: RoleEnum('roles').default('user').notNull(),\n banned: boolean('banned').default(false).notNull(),\n bannedAt: timestamp('bannedAt'),\n bannedReason: text('bannedReason'),\n // Required by better-auth's admin plugin schema (auto-expiring bans). We\n // never set this ourselves, so it stays null and bans behave as permanent,\n // same as before the plugin was added.\n banExpires: timestamp('banExpires'),\n lastLoginAt: timestamp('lastLoginAt'),\n lastActiveAt: timestamp('lastActiveAt'),\n // Set by better-auth's twoFactor plugin — true once the user has\n // completed TOTP setup (see twoFactor pgTable below for the secret /\n // backup codes themselves).\n twoFactorEnabled: boolean('twoFactorEnabled').notNull().default(false),\n createdAt: timestamp('createdAt').notNull().defaultNow(),\n updatedAt: timestamp('updatedAt').notNull().defaultNow(),\n});\n\nexport const sessions = pgTable(\"session\", {\n id: text(\"id\").notNull().primaryKey().$defaultFn(() => createId()),\n expiresAt: timestamp(\"expiresAt\").notNull(),\n token: text(\"token\").notNull().unique(),\n createdAt: timestamp(\"createdAt\").notNull().defaultNow(),\n updatedAt: timestamp(\"updatedAt\").notNull().defaultNow(),\n ipAddress: text(\"ipAddress\"),\n userAgent: text(\"userAgent\"),\n userId: text(\"userId\")\n .notNull()\n .references(() => users.id, { onDelete: \"cascade\" }),\n // Set by better-auth's admin plugin while this session is an admin\n // impersonating another user; holds the impersonating admin's user id.\n impersonatedBy: text(\"impersonatedBy\"),\n});\n\nexport const accounts = pgTable(\"account\", {\n id: text(\"id\").notNull().primaryKey().$defaultFn(() => createId()),\n accountId: text(\"accountId\").notNull(),\n providerId: text(\"providerId\").notNull(),\n userId: text(\"userId\")\n .notNull()\n .references(() => users.id, { onDelete: \"cascade\" }),\n accessToken: text(\"accessToken\"),\n refreshToken: text(\"refreshToken\"),\n idToken: text(\"idToken\"),\n accessTokenExpiresAt: timestamp(\"accessTokenExpiresAt\"),\n refreshTokenExpiresAt: timestamp(\"refreshTokenExpiresAt\"),\n scope: text(\"scope\"),\n password: text(\"password\"),\n createdAt: timestamp(\"createdAt\").notNull().defaultNow(),\n updatedAt: timestamp(\"updatedAt\").notNull().defaultNow(),\n});\n\n// Better-auth's twoFactor plugin. One row per user (created on first\n// `enable`); secret and backupCodes are its own encrypted-at-rest strings,\n// field names/types must match its expectations exactly (drizzleAdapter in\n// server/auth.ts maps this table to the plugin's \"twoFactor\" model as-is).\nexport const twoFactors = pgTable(\"twoFactor\", {\n id: text(\"id\").notNull().primaryKey().$defaultFn(() => createId()),\n secret: text(\"secret\").notNull(),\n backupCodes: text(\"backupCodes\").notNull(),\n userId: text(\"userId\")\n .notNull()\n .references(() => users.id, { onDelete: \"cascade\" }),\n});\n\n// Better-auth's passkey plugin — one row per registered WebAuthn credential.\nexport const passkeys = pgTable(\"passkey\", {\n id: text(\"id\").notNull().primaryKey().$defaultFn(() => createId()),\n name: text(\"name\"),\n publicKey: text(\"publicKey\").notNull(),\n userId: text(\"userId\")\n .notNull()\n .references(() => users.id, { onDelete: \"cascade\" }),\n credentialID: text(\"credentialID\").notNull().unique(),\n counter: integer(\"counter\").notNull(),\n deviceType: text(\"deviceType\").notNull(),\n backedUp: boolean(\"backedUp\").notNull(),\n transports: text(\"transports\"),\n createdAt: timestamp(\"createdAt\"),\n aaguid: text(\"aaguid\"),\n});\n\nexport const verifications = pgTable(\"verification\", {\n id: text(\"id\").notNull().primaryKey().$defaultFn(() => createId()),\n identifier: text(\"identifier\").notNull(),\n value: text(\"value\").notNull(),\n expiresAt: timestamp(\"expiresAt\").notNull(),\n createdAt: timestamp(\"createdAt\").notNull().defaultNow(),\n updatedAt: timestamp(\"updatedAt\").notNull().defaultNow(),\n});\n\nexport const activityLogs = pgTable(\"activity_log\", {\n id: text(\"id\").notNull().primaryKey().$defaultFn(() => createId()),\n userId: text(\"userId\")\n .notNull()\n .references(() => users.id, { onDelete: \"cascade\" }),\n action: ActivityActionEnum('action').notNull(),\n description: text('description').notNull(),\n ipAddress: text('ipAddress'),\n userAgent: text('userAgent'),\n metadata: text('metadata'),\n // Populated on 'login' events only (registration + sign-in) — parsed from\n // userAgent/ipAddress at write time so the record stays stable even if\n // the parsing library or geoip database changes later.\n referrerUrl: text('referrerUrl'),\n os: text('os'),\n browser: text('browser'),\n deviceType: text('deviceType'),\n country: text('country'),\n city: text('city'),\n createdAt: timestamp('createdAt').notNull().defaultNow(),\n});\n\n// One row per (user, device fingerprint) pair. Its only job is to answer\n// \"has this account signed in from this device before?\" — no matching row ⇒\n// send a new-device alert (see app/api/device-check/route.ts). visitorId is\n// FingerprintJS's identifier, computed client-side in\n// components/auth/deviceCheck.tsx. UA+IP are deliberately NOT the signal here:\n// they churn constantly on mobile and would alert on every other login.\nexport const deviceFingerprints = pgTable(\"device_fingerprint\", {\n id: text(\"id\").notNull().primaryKey().$defaultFn(() => createId()),\n userId: text(\"userId\")\n .notNull()\n .references(() => users.id, { onDelete: \"cascade\" }),\n visitorId: text(\"visitorId\").notNull(),\n // Context captured at first-seen time — for the admin panel's \"New devices\"\n // card and the alert email. Not part of the new-vs-known decision (that's\n // visitorId only), just so an admin can eyeball whether a sign-in looks like\n // a hijack. Nullable: an old row predates these columns, or geoip missed.\n ipAddress: text(\"ipAddress\"),\n userAgent: text(\"userAgent\"),\n country: text(\"country\"),\n city: text(\"city\"),\n createdAt: timestamp(\"createdAt\").notNull().defaultNow(),\n}, (table) => [\n unique(\"device_fingerprint_user_visitor\").on(table.userId, table.visitorId),\n]);\n\n// Per-device abuse throttle for /sign-in/email and /sign-up/email — one\n// fixed-window counter row per FingerprintJS visitorId (sent by the auth\n// forms as the x-device-fingerprint header; see lib/auth-throttle.ts).\n// Keyed by device, not IP: IP is shared behind corporate NAT / CGNAT / VPN,\n// so an IP counter throttles coworkers together while missing an abuser who\n// rotates IPs. Rows whose window has elapsed are dead weight but harmless\n// (reset on the next attempt) — prune on a cron if the table ever grows.\nexport const authThrottle = pgTable(\"auth_throttle\", {\n fingerprint: text(\"fingerprint\").notNull().primaryKey(),\n count: integer(\"count\").notNull().default(0),\n windowStart: timestamp(\"windowStart\").notNull().defaultNow(),\n // Blockable data points, overwritten on every attempt (last-write-wins) so\n // the admin panel can show who to block — by IP or by fingerprint — with no\n // join and no per-attempt history table. windowStart anchors the counter's\n // window; lastAttemptAt is just \"most recently seen\".\n lastAttemptAt: timestamp(\"lastAttemptAt\").notNull().defaultNow(),\n ipAddress: text(\"ipAddress\"),\n userAgent: text(\"userAgent\"),\n lastEmail: text(\"lastEmail\"),\n lastKind: text(\"lastKind\"), // 'signin' | 'signup'\n});\n\n// Singleton row (id is always 'default') holding admin-editable settings for\n// config that used to be env-var-only. See lib/settings-queries.ts for the\n// cached read path and lazy-seed-from-env behavior.\nexport const systemSettings = pgTable(\"system_settings\", {\n id: text(\"id\").primaryKey().default(\"default\"),\n maintenanceMode: boolean(\"maintenanceMode\").notNull().default(false),\n maintenanceMessage: text(\"maintenanceMessage\"),\n authEnableGoogle: boolean(\"authEnableGoogle\").notNull().default(true),\n authEnableEmailPassword: boolean(\"authEnableEmailPassword\").notNull().default(true),\n authEnableOneTap: boolean(\"authEnableOneTap\").notNull().default(false),\n enableSessionRevocation: boolean(\"enableSessionRevocation\").notNull().default(true),\n updatedAt: timestamp(\"updatedAt\").notNull().defaultNow(),\n // Who last changed a setting. This row doesn't \"belong\" to that admin the\n // way sessions/activityLogs belong to their user, so deleting that admin's\n // account should null this out rather than cascade-delete the singleton row.\n updatedBy: text(\"updatedBy\").references(() => users.id, { onDelete: \"set null\" }),\n}, (table) => [\n check(\"system_settings_singleton\", sql`${table.id} = 'default'`),\n]);","// No \"server-only\" guard here — this schema is also imported by scripts that\n// run outside Next.js's bundler (e.g. scripts/doctor.ts), where that guard\n// throws unconditionally. config/env.ts re-exports the parsed, guarded result\n// for app code; import this file directly only from non-Next scripts.\nimport { z } from 'zod';\n\nexport const envSchema = z.object({\n NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n PORT: z.string().default('3000').transform(Number),\n DATABASE_URL: z.string().default(''),\n RESEND_API_KEY: z.string().default(''),\n // Display name for this deployment — shown as the authenticator-app label\n // (2FA issuer) and the passkey relying-party name. Consuming projects set\n // this in their own .env; defaults to the template name.\n APP_NAME: z.string().default('Next.js Template'),\n // Seed-only: the live value lives in the system_settings DB row (see\n // lib/settings-queries.ts) once it's been seeded on first read, and is\n // changed from the admin System Settings page, not by editing this var.\n ENABLE_SESSION_REVOCATION: z.string().default('true').transform(val => val === 'true'),\n AUTH_ENABLE_GOOGLE: z.string().default('true').transform(val => val === 'true'),\n AUTH_ENABLE_EMAIL_PASSWORD: z.string().default('true').transform(val => val === 'true'),\n AUTH_ENABLE_ONE_TAP: z.string().default('false').transform(val => val === 'true'),\n DB_DRIVER: z.enum(['pg', 'neon']).default('pg'),\n // Only relevant when DB_DRIVER=pg — whether the target Postgres server has SSL\n // enabled. Self-hosted Postgres (e.g. via Docker/Dokploy) typically does not.\n DATABASE_SSL: z.string().default('false').transform(val => val === 'true'),\n});\n","import \"server-only\";\nimport { envSchema } from './env-schema';\n\nconst results = envSchema.safeParse(process.env);\n\nif (results.success && (results.data.NODE_ENV === 'development')) {\n console.log('Environment variables loaded:', Object.keys(results.data));\n}\n\nif (!results.success) {\n console.error('Invalid environment variables:', results.error.format());\n throw new Error('Invalid environment variables');\n}\n\nexport const config = results.data;\n","import 'server-only';\n\n// neon-http driver for production\nimport { neon } from '@neondatabase/serverless';\nimport { drizzle } from 'drizzle-orm/neon-http';\n\n// postgres driver for local development\nimport { drizzle as drizzlePg } from 'drizzle-orm/node-postgres';\nimport { Pool } from 'pg';\n\nimport * as schema from './schema';\nimport { config } from '../../config/env';\n\n// Import the query builder\nimport { PostgresJsDatabase } from 'drizzle-orm/postgres-js';\nimport { NeonHttpDatabase } from 'drizzle-orm/neon-http';\n\ntype DrizzleNeon = NeonHttpDatabase<typeof schema>;\ntype DrizzlePg = PostgresJsDatabase<typeof schema>;\n\nlet db: DrizzleNeon | DrizzlePg\n\n\nif (config.DB_DRIVER === 'pg') {\n const pool = new Pool({\n connectionString: config.DATABASE_URL!,\n ssl: config.DATABASE_SSL ? { rejectUnauthorized: false } : undefined,\n });\n db = drizzlePg(pool, {\n schema,\n logger: true,\n });\n\n console.log('🟡 Using PostgreSQL (pg) driver');\n\n} else if (config.DB_DRIVER === 'neon') {\n const sql = neon(config.DATABASE_URL!);\n db = drizzle(sql, { schema, logger: true });\n\n console.log('🟢 Using Neon (HTTP) driver');\n\n} else {\n console.log('Database connection not established, unknown DB_DRIVER:', config.DB_DRIVER);\n}\n// Note: In production, you might want to disable logging or use a more sophisticated logger.\n\nexport { db }","import \"server-only\"\nimport { db } from \"../server/db\"\nimport { users } from \"../server/db/schema\"\nimport { like, desc, or, eq, sql, type SQL } from \"drizzle-orm\"\n\nexport async function getUserById(id: string) {\n const result = await db\n .select()\n .from(users)\n .where(eq(users.id, id))\n .limit(1)\n\n return result[0] || null\n}\n\n// Recipients for security alerts (new-device sign-ins, etc.). Every admin.\nexport async function getAdminEmails(): Promise<string[]> {\n const rows = await db\n .select({ email: users.email })\n .from(users)\n .where(eq(users.role, \"admin\"))\n\n return rows.map((r) => r.email)\n}\n\nexport interface UserFilters {\n search?: string\n page?: number\n limit?: number\n}\n\nfunction buildUsersWhere(search: string): SQL | undefined {\n return search.trim()\n ? (or(\n like(users.name, `%${search}%`),\n like(users.email, `%${search}%`)\n ) as SQL)\n : undefined\n}\n\nexport async function getUsersFiltered({ search = \"\", page = 1, limit = 10 }: UserFilters) {\n const offset = (page - 1) * limit\n\n const where = buildUsersWhere(search)\n\n const [usersList, countResult] = await Promise.all([\n db\n .select()\n .from(users)\n .where(where)\n .orderBy(desc(users.createdAt))\n .limit(limit)\n .offset(offset),\n db\n .select({ count: sql<number>`count(*)` })\n .from(users)\n .where(where),\n ])\n\n const total = Number(countResult[0]?.count ?? 0)\n\n return {\n users: usersList,\n total,\n page,\n limit,\n pages: Math.ceil(total / limit),\n }\n}\n\nexport interface UserExportFilters {\n search?: string\n}\n\n// Caps export size to keep the CSV response bounded on very large tables.\nconst EXPORT_ROW_LIMIT = 10_000\n\nexport async function getUsersForExport({ search = \"\" }: UserExportFilters) {\n const where = buildUsersWhere(search)\n\n return db\n .select()\n .from(users)\n .where(where)\n .orderBy(desc(users.createdAt))\n .limit(EXPORT_ROW_LIMIT)\n}\n"]}
@@ -0,0 +1,47 @@
1
+ import { db } from '../chunk-HYKUB3A2.mjs';
2
+ import { users } from '../chunk-ENGYO4K7.mjs';
3
+ import '../chunk-GTPTVNY2.mjs';
4
+ import '../chunk-YN4436PK.mjs';
5
+ import 'server-only';
6
+ import { eq, desc, sql, or, like } from 'drizzle-orm';
7
+
8
+ async function getUserById(id) {
9
+ const result = await db.select().from(users).where(eq(users.id, id)).limit(1);
10
+ return result[0] || null;
11
+ }
12
+ async function getAdminEmails() {
13
+ const rows = await db.select({ email: users.email }).from(users).where(eq(users.role, "admin"));
14
+ return rows.map((r) => r.email);
15
+ }
16
+ function buildUsersWhere(search) {
17
+ return search.trim() ? or(
18
+ like(users.name, `%${search}%`),
19
+ like(users.email, `%${search}%`)
20
+ ) : void 0;
21
+ }
22
+ async function getUsersFiltered({ search = "", page = 1, limit = 10 }) {
23
+ var _a, _b;
24
+ const offset = (page - 1) * limit;
25
+ const where = buildUsersWhere(search);
26
+ const [usersList, countResult] = await Promise.all([
27
+ db.select().from(users).where(where).orderBy(desc(users.createdAt)).limit(limit).offset(offset),
28
+ db.select({ count: sql`count(*)` }).from(users).where(where)
29
+ ]);
30
+ const total = Number((_b = (_a = countResult[0]) == null ? void 0 : _a.count) != null ? _b : 0);
31
+ return {
32
+ users: usersList,
33
+ total,
34
+ page,
35
+ limit,
36
+ pages: Math.ceil(total / limit)
37
+ };
38
+ }
39
+ var EXPORT_ROW_LIMIT = 1e4;
40
+ async function getUsersForExport({ search = "" }) {
41
+ const where = buildUsersWhere(search);
42
+ return db.select().from(users).where(where).orderBy(desc(users.createdAt)).limit(EXPORT_ROW_LIMIT);
43
+ }
44
+
45
+ export { getAdminEmails, getUserById, getUsersFiltered, getUsersForExport };
46
+ //# sourceMappingURL=queries.mjs.map
47
+ //# sourceMappingURL=queries.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../lib/user-queries.ts"],"names":[],"mappings":";;;;;;;AAKA,eAAsB,YAAY,EAAA,EAAY;AAC5C,EAAA,MAAM,SAAS,MAAM,EAAA,CAClB,MAAA,EAAO,CACP,KAAK,KAAK,CAAA,CACV,KAAA,CAAM,EAAA,CAAG,MAAM,EAAA,EAAI,EAAE,CAAC,CAAA,CACtB,MAAM,CAAC,CAAA;AAEV,EAAA,OAAO,MAAA,CAAO,CAAC,CAAA,IAAK,IAAA;AACtB;AAGA,eAAsB,cAAA,GAAoC;AACxD,EAAA,MAAM,OAAO,MAAM,EAAA,CAChB,OAAO,EAAE,KAAA,EAAO,MAAM,KAAA,EAAO,CAAA,CAC7B,IAAA,CAAK,KAAK,CAAA,CACV,KAAA,CAAM,GAAG,KAAA,CAAM,IAAA,EAAM,OAAO,CAAC,CAAA;AAEhC,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,KAAK,CAAA;AAChC;AAQA,SAAS,gBAAgB,MAAA,EAAiC;AACxD,EAAA,OAAO,MAAA,CAAO,MAAK,GACd,EAAA;AAAA,IACC,IAAA,CAAK,KAAA,CAAM,IAAA,EAAM,CAAA,CAAA,EAAI,MAAM,CAAA,CAAA,CAAG,CAAA;AAAA,IAC9B,IAAA,CAAK,KAAA,CAAM,KAAA,EAAO,CAAA,CAAA,EAAI,MAAM,CAAA,CAAA,CAAG;AAAA,GACjC,GACA,MAAA;AACN;AAEA,eAAsB,gBAAA,CAAiB,EAAE,MAAA,GAAS,EAAA,EAAI,OAAO,CAAA,EAAG,KAAA,GAAQ,IAAG,EAAgB;AAxC3F,EAAA,IAAA,EAAA,EAAA,EAAA;AAyCE,EAAA,MAAM,MAAA,GAAA,CAAU,OAAO,CAAA,IAAK,KAAA;AAE5B,EAAA,MAAM,KAAA,GAAQ,gBAAgB,MAAM,CAAA;AAEpC,EAAA,MAAM,CAAC,SAAA,EAAW,WAAW,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,IACjD,GACG,MAAA,EAAO,CACP,KAAK,KAAK,CAAA,CACV,MAAM,KAAK,CAAA,CACX,QAAQ,IAAA,CAAK,KAAA,CAAM,SAAS,CAAC,CAAA,CAC7B,MAAM,KAAK,CAAA,CACX,OAAO,MAAM,CAAA;AAAA,IAChB,EAAA,CACG,MAAA,CAAO,EAAE,KAAA,EAAO,GAAA,CAAA,QAAA,CAAA,EAAuB,CAAA,CACvC,IAAA,CAAK,KAAK,CAAA,CACV,KAAA,CAAM,KAAK;AAAA,GACf,CAAA;AAED,EAAA,MAAM,KAAA,GAAQ,QAAO,EAAA,GAAA,CAAA,EAAA,GAAA,WAAA,CAAY,CAAC,MAAb,IAAA,GAAA,MAAA,GAAA,EAAA,CAAgB,KAAA,KAAhB,YAAyB,CAAC,CAAA;AAE/C,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,SAAA;AAAA,IACP,KAAA;AAAA,IACA,IAAA;AAAA,IACA,KAAA;AAAA,IACA,KAAA,EAAO,IAAA,CAAK,IAAA,CAAK,KAAA,GAAQ,KAAK;AAAA,GAChC;AACF;AAOA,IAAM,gBAAA,GAAmB,GAAA;AAEzB,eAAsB,iBAAA,CAAkB,EAAE,MAAA,GAAS,EAAA,EAAG,EAAsB;AAC1E,EAAA,MAAM,KAAA,GAAQ,gBAAgB,MAAM,CAAA;AAEpC,EAAA,OAAO,GACJ,MAAA,EAAO,CACP,IAAA,CAAK,KAAK,EACV,KAAA,CAAM,KAAK,CAAA,CACX,OAAA,CAAQ,KAAK,KAAA,CAAM,SAAS,CAAC,CAAA,CAC7B,MAAM,gBAAgB,CAAA;AAC3B","file":"queries.mjs","sourcesContent":["import \"server-only\"\nimport { db } from \"../server/db\"\nimport { users } from \"../server/db/schema\"\nimport { like, desc, or, eq, sql, type SQL } from \"drizzle-orm\"\n\nexport async function getUserById(id: string) {\n const result = await db\n .select()\n .from(users)\n .where(eq(users.id, id))\n .limit(1)\n\n return result[0] || null\n}\n\n// Recipients for security alerts (new-device sign-ins, etc.). Every admin.\nexport async function getAdminEmails(): Promise<string[]> {\n const rows = await db\n .select({ email: users.email })\n .from(users)\n .where(eq(users.role, \"admin\"))\n\n return rows.map((r) => r.email)\n}\n\nexport interface UserFilters {\n search?: string\n page?: number\n limit?: number\n}\n\nfunction buildUsersWhere(search: string): SQL | undefined {\n return search.trim()\n ? (or(\n like(users.name, `%${search}%`),\n like(users.email, `%${search}%`)\n ) as SQL)\n : undefined\n}\n\nexport async function getUsersFiltered({ search = \"\", page = 1, limit = 10 }: UserFilters) {\n const offset = (page - 1) * limit\n\n const where = buildUsersWhere(search)\n\n const [usersList, countResult] = await Promise.all([\n db\n .select()\n .from(users)\n .where(where)\n .orderBy(desc(users.createdAt))\n .limit(limit)\n .offset(offset),\n db\n .select({ count: sql<number>`count(*)` })\n .from(users)\n .where(where),\n ])\n\n const total = Number(countResult[0]?.count ?? 0)\n\n return {\n users: usersList,\n total,\n page,\n limit,\n pages: Math.ceil(total / limit),\n }\n}\n\nexport interface UserExportFilters {\n search?: string\n}\n\n// Caps export size to keep the CSV response bounded on very large tables.\nconst EXPORT_ROW_LIMIT = 10_000\n\nexport async function getUsersForExport({ search = \"\" }: UserExportFilters) {\n const where = buildUsersWhere(search)\n\n return db\n .select()\n .from(users)\n .where(where)\n .orderBy(desc(users.createdAt))\n .limit(EXPORT_ROW_LIMIT)\n}\n"]}
@@ -0,0 +1,58 @@
1
+ import { createAuthClient } from "better-auth/react"
2
+ import { oneTapClient, adminClient, twoFactorClient, passkeyClient } from "better-auth/client/plugins"
3
+ import type { BetterAuthClientPlugin } from "better-auth/client"
4
+
5
+ const oneTapEnabled = process.env.NEXT_PUBLIC_AUTH_ENABLE_ONE_TAP === "true"
6
+
7
+ // Derived (not hand-written) from the plugin's own return type, so it tracks
8
+ // whatever better-auth version is actually installed by the consumer.
9
+ type OneTapActions = ReturnType<ReturnType<typeof oneTapClient>["getActions"]>
10
+
11
+ const baseAuthClient = createAuthClient({
12
+ baseURL: typeof window !== "undefined" ? window.location.origin : undefined,
13
+
14
+ plugins: [
15
+ adminClient(),
16
+ twoFactorClient(),
17
+ passkeyClient(),
18
+ ...(oneTapEnabled ? [
19
+ // On newer better-auth versions (>=1.6, within our ^1.3.34 range) the
20
+ // one-tap plugin's `getActions` signature doesn't structurally satisfy
21
+ // `BetterAuthClientPlugin` (a generic-variance mismatch upstream), so
22
+ // it can't be assigned into this array as-is. Widen it here —
23
+ // `.oneTap` is added back to `authClient`'s type below via
24
+ // `OneTapActions`, independent of this array's inferred plugin type.
25
+ oneTapClient({
26
+ clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "",
27
+ // Optional client configuration:
28
+ autoSelect: false,
29
+ cancelOnTapOutside: true,
30
+ context: "signin",
31
+ additionalOptions: {
32
+ // Any extra options for the Google initialize method
33
+ },
34
+ // Configure prompt behavior and exponential backoff:
35
+ promptOptions: {
36
+ baseDelay: 1000, // Base delay in ms (default: 1000)
37
+ maxAttempts: 5 // Maximum number of attempts before triggering onPromptNotification (default: 5)
38
+ }
39
+ }) as unknown as BetterAuthClientPlugin
40
+ ] : []),
41
+ ]
42
+ })
43
+
44
+ // createAuthClient can't narrow `Option["plugins"]` from a conditional spread,
45
+ // so it loses the one-tap plugin's action types. Add `.oneTap` back here
46
+ // instead of forcing the plugin into the array unconditionally — doing that
47
+ // trips a getActions variance mismatch against BetterAuthClientPlugin on
48
+ // newer better-auth versions (>=1.6). The env check above still fully gates
49
+ // whether the plugin (and thus `.oneTap`) is registered at runtime.
50
+ export const authClient = baseAuthClient as typeof baseAuthClient & OneTapActions
51
+
52
+ // Export commonly used hooks and methods for convenience
53
+ export const {
54
+ useSession,
55
+ signIn,
56
+ signOut,
57
+ signUp,
58
+ } = authClient
@@ -0,0 +1,13 @@
1
+ // Shared cookie/header names used by both client components and server/auth.ts
2
+ // — no "server-only" guard, since both sides need them.
3
+
4
+ // Stashes the page a social sign-in was initiated from, so the login-activity
5
+ // log shows the real referrer instead of the OAuth provider's own domain
6
+ // (e.g. accounts.google.com) once the browser redirects back.
7
+ export const LOGIN_REFERRER_COOKIE = 'login_referrer'
8
+
9
+ // Carries the client's FingerprintJS visitorId on /sign-in/email and
10
+ // /sign-up/email requests — the key for the per-device abuse throttle
11
+ // (see lib/auth-throttle.ts). A header, not a cookie, so it's only sent on
12
+ // the auth calls that need it, not every request.
13
+ export const DEVICE_FINGERPRINT_HEADER = 'x-device-fingerprint'
@@ -0,0 +1,21 @@
1
+ // Shared client-side device id (FingerprintJS visitorId), computed once and
2
+ // cached for the page's lifetime — the library's get() isn't cheap and the
3
+ // login form, signup form and deviceCheck.tsx all want the same value.
4
+ //
5
+ // @fingerprintjs/fingerprintjs is an optional peer dep: if it isn't installed
6
+ // (or is blocked in the browser), this resolves to null and callers carry on
7
+ // without a fingerprint.
8
+ let cached: Promise<string | null> | undefined
9
+
10
+ export function getVisitorId(): Promise<string | null> {
11
+ cached ??= (async () => {
12
+ try {
13
+ const FingerprintJS = (await import("@fingerprintjs/fingerprintjs")).default
14
+ const { visitorId } = await (await FingerprintJS.load()).get()
15
+ return visitorId || null
16
+ } catch {
17
+ return null
18
+ }
19
+ })()
20
+ return cached
21
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Format date consistently across dashboard
3
+ */
4
+ export function formatDate(date: Date | string): string {
5
+ return new Date(date).toLocaleDateString('en-US', {
6
+ year: 'numeric',
7
+ month: 'long',
8
+ day: 'numeric',
9
+ })
10
+ }
11
+
12
+ /**
13
+ * Format date and time
14
+ */
15
+ export function formatDateTime(date: Date | string): string {
16
+ return new Date(date).toLocaleString('en-US', {
17
+ year: 'numeric',
18
+ month: 'long',
19
+ day: 'numeric',
20
+ hour: '2-digit',
21
+ minute: '2-digit',
22
+ })
23
+ }
24
+
25
+ /**
26
+ * Get user initials for avatar fallback
27
+ */
28
+ export function getUserInitials(name: string | null | undefined): string {
29
+ if (!name) return 'U'
30
+
31
+ const parts = name.trim().split(' ')
32
+ if (parts.length === 1) return parts[0].charAt(0).toUpperCase()
33
+
34
+ return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase()
35
+ }
36
+
37
+ /**
38
+ * Badge variant per activity log action, shared by activity log tables
39
+ */
40
+ export const actionBadgeVariant: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
41
+ login: 'default',
42
+ logout: 'secondary',
43
+ login_failed: 'destructive',
44
+ password_changed: 'default',
45
+ email_changed: 'default',
46
+ profile_updated: 'secondary',
47
+ session_revoked: 'destructive',
48
+ user_deleted: 'destructive',
49
+ user_banned: 'destructive',
50
+ user_unbanned: 'default',
51
+ role_changed: 'default',
52
+ settings_changed: 'default',
53
+ }