@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,319 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../../ui/card'
5
+ import { Button } from '../../ui/button'
6
+ import { Input } from '../../ui/input'
7
+ import { Label } from '../../ui/label'
8
+ import { Badge } from '../../ui/badge'
9
+ import { Skeleton } from '../../ui/skeleton'
10
+ import { DataTable, DataTablePagination, type Column } from '../../ui/data-table'
11
+ import { toast } from 'sonner'
12
+ import { formatDateTime } from '@xk2800/nextjs-template/lib/formatters'
13
+ import { Download } from 'lucide-react'
14
+
15
+ interface ActivityLog {
16
+ id: string
17
+ action: string
18
+ description: string
19
+ ipAddress: string | null
20
+ userAgent: string | null
21
+ metadata: string | null
22
+ createdAt: Date | string
23
+ userId: string
24
+ userName: string | null
25
+ userEmail: string | null
26
+ }
27
+
28
+ interface PaginationData {
29
+ page: number
30
+ limit: number
31
+ total: number
32
+ pages: number
33
+ }
34
+
35
+ const actionOptions = [
36
+ { value: '', label: 'All actions' },
37
+ { value: 'login', label: 'Login' },
38
+ { value: 'logout', label: 'Logout' },
39
+ { value: 'login_failed', label: 'Login failed' },
40
+ { value: 'password_changed', label: 'Password changed' },
41
+ { value: 'email_changed', label: 'Email changed' },
42
+ { value: 'profile_updated', label: 'Profile updated' },
43
+ { value: 'session_revoked', label: 'Session revoked' },
44
+ { value: 'user_deleted', label: 'User deleted' },
45
+ { value: 'user_banned', label: 'User banned' },
46
+ { value: 'user_unbanned', label: 'User unbanned' },
47
+ { value: 'role_changed', label: 'Role changed' },
48
+ { value: 'impersonation_started', label: 'Impersonation started' },
49
+ { value: 'impersonation_stopped', label: 'Impersonation stopped' },
50
+ { value: 'settings_changed', label: 'Settings changed' },
51
+ ]
52
+
53
+ const actionBadgeVariant: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
54
+ login: 'default',
55
+ logout: 'secondary',
56
+ login_failed: 'destructive',
57
+ password_changed: 'default',
58
+ email_changed: 'default',
59
+ profile_updated: 'secondary',
60
+ session_revoked: 'destructive',
61
+ user_deleted: 'destructive',
62
+ user_banned: 'destructive',
63
+ user_unbanned: 'default',
64
+ role_changed: 'default',
65
+ impersonation_started: 'destructive',
66
+ impersonation_stopped: 'secondary',
67
+ settings_changed: 'default',
68
+ }
69
+
70
+ interface AdminActivityLogsTableProps {
71
+ initialLogs: ActivityLog[]
72
+ initialPagination: PaginationData
73
+ }
74
+
75
+ const columns: Column<ActivityLog>[] = [
76
+ {
77
+ name: 'action',
78
+ title: 'Action',
79
+ sticky: true,
80
+ renderer: (log) => (
81
+ <Badge variant={actionBadgeVariant[log.action] || 'outline'}>
82
+ {log.action.replace(/_/g, ' ')}
83
+ </Badge>
84
+ ),
85
+ },
86
+ {
87
+ name: 'user',
88
+ title: 'User',
89
+ renderer: (log) => (
90
+ <div className="text-sm">
91
+ <div className="font-medium">{log.userName || 'N/A'}</div>
92
+ <div className="text-xs text-gray-500">{log.userEmail}</div>
93
+ </div>
94
+ ),
95
+ },
96
+ {
97
+ name: 'description',
98
+ title: 'Description',
99
+ renderer: (log) => (
100
+ <span className="text-sm max-w-xs truncate block">{log.description}</span>
101
+ ),
102
+ },
103
+ {
104
+ name: 'ipAddress',
105
+ title: 'IP Address',
106
+ renderer: (log) => <span className="text-sm font-mono">{log.ipAddress || 'N/A'}</span>,
107
+ },
108
+ {
109
+ name: 'createdAt',
110
+ title: 'Time',
111
+ renderer: (log) => (
112
+ <span className="text-sm text-gray-600 dark:text-gray-400">
113
+ {formatDateTime(log.createdAt)}
114
+ </span>
115
+ ),
116
+ },
117
+ ]
118
+
119
+ export default function AdminActivityLogsTable({ initialLogs, initialPagination }: AdminActivityLogsTableProps) {
120
+ const [logs, setLogs] = useState<ActivityLog[]>(initialLogs)
121
+ const [loading, setLoading] = useState(false)
122
+ const [search, setSearch] = useState('')
123
+ const [action, setAction] = useState('')
124
+ const [dateFrom, setDateFrom] = useState('')
125
+ const [dateTo, setDateTo] = useState('')
126
+ const [page, setPage] = useState(1)
127
+ const [pagination, setPagination] = useState<PaginationData | null>(initialPagination)
128
+ const [isExporting, setIsExporting] = useState(false)
129
+
130
+ const requestId = useRef(0)
131
+
132
+ const fetchLogs = async (pageNum: number) => {
133
+ const currentRequest = ++requestId.current
134
+ setLoading(true)
135
+ try {
136
+ const params = new URLSearchParams({
137
+ page: pageNum.toString(),
138
+ limit: '20',
139
+ search,
140
+ action,
141
+ ...(dateFrom && { dateFrom }),
142
+ ...(dateTo && { dateTo }),
143
+ })
144
+ const response = await fetch(`/api/admin/activity-logs?${params}`)
145
+ if (!response.ok) throw new Error('Failed to fetch activity logs')
146
+ const data = await response.json()
147
+ if (currentRequest !== requestId.current) return
148
+ setLogs(data.logs)
149
+ setPagination(data.pagination)
150
+ } catch (error) {
151
+ if (currentRequest !== requestId.current) return
152
+ toast.error('Failed to load activity logs')
153
+ console.error(error)
154
+ } finally {
155
+ if (currentRequest === requestId.current) setLoading(false)
156
+ }
157
+ }
158
+
159
+ const isFirstRender = useRef(true)
160
+
161
+ useEffect(() => {
162
+ if (isFirstRender.current) {
163
+ isFirstRender.current = false
164
+ return
165
+ }
166
+ const timer = setTimeout(() => {
167
+ fetchLogs(1)
168
+ setPage(1)
169
+ }, 300)
170
+ return () => clearTimeout(timer)
171
+ // eslint-disable-next-line react-hooks/exhaustive-deps
172
+ }, [search, action, dateFrom, dateTo])
173
+
174
+ const resetFilters = () => {
175
+ setSearch('')
176
+ setAction('')
177
+ setDateFrom('')
178
+ setDateTo('')
179
+ }
180
+
181
+ const handleExport = async () => {
182
+ setIsExporting(true)
183
+ try {
184
+ const params = new URLSearchParams({
185
+ search,
186
+ action,
187
+ ...(dateFrom && { dateFrom }),
188
+ ...(dateTo && { dateTo }),
189
+ })
190
+ const response = await fetch(`/api/admin/activity-logs/export?${params}`)
191
+ if (!response.ok) throw new Error('Failed to export activity logs')
192
+ const blob = await response.blob()
193
+ const url = URL.createObjectURL(blob)
194
+ const link = document.createElement('a')
195
+ link.href = url
196
+ link.download = `activity-logs-${new Date().toISOString().slice(0, 10)}.csv`
197
+ document.body.appendChild(link)
198
+ link.click()
199
+ link.remove()
200
+ URL.revokeObjectURL(url)
201
+ } catch (error) {
202
+ toast.error('Failed to export activity logs')
203
+ console.error(error)
204
+ } finally {
205
+ setIsExporting(false)
206
+ }
207
+ }
208
+
209
+ return (
210
+ <>
211
+ <Card>
212
+ <CardHeader>
213
+ <CardTitle>Filters</CardTitle>
214
+ <CardDescription>Search and narrow down activity events</CardDescription>
215
+ </CardHeader>
216
+ <CardContent>
217
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 items-end">
218
+ <div className="space-y-2 lg:col-span-2">
219
+ <Label htmlFor="search">User</Label>
220
+ <Input
221
+ id="search"
222
+ placeholder="Search by name or email..."
223
+ value={search}
224
+ onChange={(e) => setSearch(e.target.value)}
225
+ />
226
+ </div>
227
+
228
+ <div className="space-y-2">
229
+ <Label htmlFor="action">Action</Label>
230
+ <select
231
+ id="action"
232
+ value={action}
233
+ onChange={(e) => setAction(e.target.value)}
234
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:bg-input/30"
235
+ >
236
+ {actionOptions.map((opt) => (
237
+ <option key={opt.value} value={opt.value}>
238
+ {opt.label}
239
+ </option>
240
+ ))}
241
+ </select>
242
+ </div>
243
+
244
+ <div className="space-y-2">
245
+ <Label htmlFor="dateFrom">From</Label>
246
+ <Input
247
+ id="dateFrom"
248
+ type="date"
249
+ value={dateFrom}
250
+ onChange={(e) => setDateFrom(e.target.value)}
251
+ />
252
+ </div>
253
+
254
+ <div className="space-y-2">
255
+ <Label htmlFor="dateTo">To</Label>
256
+ <Input
257
+ id="dateTo"
258
+ type="date"
259
+ value={dateTo}
260
+ onChange={(e) => setDateTo(e.target.value)}
261
+ />
262
+ </div>
263
+ </div>
264
+
265
+ {(search || action || dateFrom || dateTo) && (
266
+ <Button variant="ghost" size="sm" className="mt-4" onClick={resetFilters}>
267
+ Clear filters
268
+ </Button>
269
+ )}
270
+ </CardContent>
271
+ </Card>
272
+
273
+ <Card>
274
+ <CardHeader className="flex flex-row items-start justify-between gap-4">
275
+ <div>
276
+ <CardTitle>Events</CardTitle>
277
+ <CardDescription>
278
+ {pagination ? `Showing ${logs.length} of ${pagination.total} events` : 'Loading...'}
279
+ </CardDescription>
280
+ </div>
281
+ <Button
282
+ variant="outline"
283
+ size="sm"
284
+ onClick={handleExport}
285
+ disabled={isExporting || logs.length === 0}
286
+ >
287
+ <Download />
288
+ {isExporting ? 'Exporting...' : 'Export CSV'}
289
+ </Button>
290
+ </CardHeader>
291
+ <CardContent>
292
+ {loading ? (
293
+ <div className="space-y-4">
294
+ {[...Array(5)].map((_, i) => (
295
+ <Skeleton key={i} className="h-16 w-full" />
296
+ ))}
297
+ </div>
298
+ ) : logs.length === 0 ? (
299
+ <p className="text-center text-gray-500 py-8">No activity logs found</p>
300
+ ) : (
301
+ <DataTable columns={columns} data={logs} getRowId={(log) => log.id} pageSize={false} />
302
+ )}
303
+
304
+ {pagination && (
305
+ <DataTablePagination
306
+ page={page}
307
+ pages={pagination.pages}
308
+ onPageChange={(p) => {
309
+ setPage(p)
310
+ fetchLogs(p)
311
+ }}
312
+ className="mt-6 pt-6"
313
+ />
314
+ )}
315
+ </CardContent>
316
+ </Card>
317
+ </>
318
+ )
319
+ }
@@ -0,0 +1,30 @@
1
+ import { Users, UserPlus, Activity, Ban } from "lucide-react"
2
+ import { getAdminDashboardStats } from "@xk2800/nextjs-template/admin/queries"
3
+ import { Card, CardContent } from "../../ui/card"
4
+
5
+ const STAT_CONFIG = [
6
+ { key: "totalUsers", label: "Total users", icon: Users },
7
+ { key: "newSignupsThisWeek", label: "New signups this week", icon: UserPlus },
8
+ { key: "activeSessions", label: "Active sessions", icon: Activity },
9
+ { key: "bannedCount", label: "Banned users", icon: Ban },
10
+ ] as const
11
+
12
+ export default async function AdminStatsGrid() {
13
+ const stats = await getAdminDashboardStats()
14
+
15
+ return (
16
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
17
+ {STAT_CONFIG.map(({ key, label, icon: Icon }) => (
18
+ <Card key={key}>
19
+ <CardContent>
20
+ <div className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
21
+ <Icon className="size-3.5" />
22
+ {label}
23
+ </div>
24
+ <p className="text-3xl font-bold mt-2 text-foreground">{stats[key]}</p>
25
+ </CardContent>
26
+ </Card>
27
+ ))}
28
+ </div>
29
+ )
30
+ }
@@ -0,0 +1,16 @@
1
+ import { Card, CardContent } from "../../ui/card"
2
+ import { Skeleton } from "../../ui/skeleton"
3
+
4
+ export default function AdminStatsGridSkeleton() {
5
+ return (
6
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
7
+ {[...Array(4)].map((_, i) => (
8
+ <Card key={i}>
9
+ <CardContent>
10
+ <Skeleton className="h-20" />
11
+ </CardContent>
12
+ </Card>
13
+ ))}
14
+ </div>
15
+ )
16
+ }
@@ -0,0 +1,53 @@
1
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../ui/card"
2
+ import { parseUserAgent, lookupGeoLocation, formatDeviceInfo, formatLocation } from "../../../lib/request-info"
3
+ import AdminUserActivityLogTable from "./adminUserActivityLogTable"
4
+
5
+ interface ActivityLog {
6
+ id: string
7
+ action: string
8
+ description: string
9
+ ipAddress: string | null
10
+ userAgent: string | null
11
+ metadata: string | null
12
+ createdAt: Date
13
+ }
14
+
15
+ function getReferrerUrl(metadata: string | null): string | null {
16
+ if (!metadata) return null
17
+
18
+ try {
19
+ const parsed = JSON.parse(metadata)
20
+ return typeof parsed.referrerUrl === 'string' ? parsed.referrerUrl : null
21
+ } catch {
22
+ return null
23
+ }
24
+ }
25
+
26
+ export default function AdminUserActivityLogCard({ logs }: { logs: ActivityLog[] }) {
27
+ const rows = logs.map((log) => ({
28
+ id: log.id,
29
+ action: log.action,
30
+ description: log.description,
31
+ referrerUrl: getReferrerUrl(log.metadata),
32
+ device: formatDeviceInfo(parseUserAgent(log.userAgent)),
33
+ location: formatLocation(lookupGeoLocation(log.ipAddress)),
34
+ ipAddress: log.ipAddress,
35
+ createdAt: log.createdAt,
36
+ }))
37
+
38
+ return (
39
+ <Card>
40
+ <CardHeader>
41
+ <CardTitle>Activity Log</CardTitle>
42
+ <CardDescription>Recent activity for this user</CardDescription>
43
+ </CardHeader>
44
+ <CardContent>
45
+ {logs.length === 0 ? (
46
+ <p className="text-center text-gray-500 py-8">No activity logs found</p>
47
+ ) : (
48
+ <AdminUserActivityLogTable rows={rows} />
49
+ )}
50
+ </CardContent>
51
+ </Card>
52
+ )
53
+ }
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+
3
+ import { Badge } from "../../ui/badge"
4
+ import { DataTable, type Column } from "../../ui/data-table"
5
+ import { formatDateTime, actionBadgeVariant } from "../../../lib/formatters"
6
+
7
+ export type ActivityLogRow = {
8
+ id: string
9
+ action: string
10
+ description: string
11
+ referrerUrl: string | null
12
+ device: string
13
+ location: string
14
+ ipAddress: string | null
15
+ createdAt: Date
16
+ }
17
+
18
+ const columns: Column<ActivityLogRow>[] = [
19
+ {
20
+ name: 'action',
21
+ title: 'Action',
22
+ sticky: true,
23
+ renderer: (log) => (
24
+ <Badge variant={actionBadgeVariant[log.action] || 'outline'}>
25
+ {log.action.replace(/_/g, ' ')}
26
+ </Badge>
27
+ ),
28
+ },
29
+ {
30
+ name: 'description',
31
+ title: 'Description',
32
+ renderer: (log) => (
33
+ <div className="text-sm max-w-xs">
34
+ <div className="truncate">{log.description}</div>
35
+ {log.referrerUrl && (
36
+ <div className="text-xs text-gray-500 truncate">via {log.referrerUrl}</div>
37
+ )}
38
+ </div>
39
+ ),
40
+ },
41
+ {
42
+ name: 'device',
43
+ title: 'Device',
44
+ renderer: (log) => <span className="text-sm">{log.device}</span>,
45
+ },
46
+ {
47
+ name: 'location',
48
+ title: 'Location',
49
+ renderer: (log) => <span className="text-sm">{log.location}</span>,
50
+ },
51
+ {
52
+ name: 'ipAddress',
53
+ title: 'IP Address',
54
+ renderer: (log) => <span className="text-sm font-mono">{log.ipAddress || 'N/A'}</span>,
55
+ },
56
+ {
57
+ name: 'createdAt',
58
+ title: 'Time',
59
+ renderer: (log) => (
60
+ <span className="text-sm text-gray-600 dark:text-gray-400">
61
+ {formatDateTime(log.createdAt)}
62
+ </span>
63
+ ),
64
+ },
65
+ ]
66
+
67
+ export default function AdminUserActivityLogTable({ rows }: { rows: ActivityLogRow[] }) {
68
+ return <DataTable columns={columns} data={rows} getRowId={(log) => log.id} />
69
+ }
@@ -0,0 +1,50 @@
1
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "../../ui/card"
2
+ import { formatDeviceInfo, formatLocation, type DeviceType } from "../../../lib/request-info"
3
+ import AdminUserLoginInfoTable from "./adminUserLoginInfoTable"
4
+
5
+ interface LoginEvent {
6
+ id: string
7
+ description: string
8
+ referrerUrl: string | null
9
+ os: string | null
10
+ browser: string | null
11
+ deviceType: string | null
12
+ country: string | null
13
+ city: string | null
14
+ ipAddress: string | null
15
+ createdAt: Date
16
+ }
17
+
18
+ export default function AdminUserLoginInfoCard({ logins }: { logins: LoginEvent[] }) {
19
+ const rows = logins.map((login) => ({
20
+ id: login.id,
21
+ isRegistration: login.description.toLowerCase().includes('registered'),
22
+ device: formatDeviceInfo({
23
+ os: login.os,
24
+ browser: login.browser,
25
+ deviceType: (login.deviceType as DeviceType) || 'desktop',
26
+ }),
27
+ location: formatLocation({ country: login.country, city: login.city }),
28
+ referrerUrl: login.referrerUrl,
29
+ ipAddress: login.ipAddress,
30
+ createdAt: login.createdAt,
31
+ }))
32
+
33
+ return (
34
+ <Card>
35
+ <CardHeader>
36
+ <CardTitle>Registration & Login Info</CardTitle>
37
+ <CardDescription>
38
+ Referrer, device, and location captured for each sign-in
39
+ </CardDescription>
40
+ </CardHeader>
41
+ <CardContent>
42
+ {logins.length === 0 ? (
43
+ <p className="text-center text-gray-500 py-8">No login history found</p>
44
+ ) : (
45
+ <AdminUserLoginInfoTable rows={rows} />
46
+ )}
47
+ </CardContent>
48
+ </Card>
49
+ )
50
+ }
@@ -0,0 +1,63 @@
1
+ 'use client'
2
+
3
+ import { Badge } from "../../ui/badge"
4
+ import { DataTable, type Column } from "../../ui/data-table"
5
+ import { formatDateTime } from "../../../lib/formatters"
6
+
7
+ export type LoginInfoRow = {
8
+ id: string
9
+ isRegistration: boolean
10
+ device: string
11
+ location: string
12
+ referrerUrl: string | null
13
+ ipAddress: string | null
14
+ createdAt: Date
15
+ }
16
+
17
+ const columns: Column<LoginInfoRow>[] = [
18
+ {
19
+ name: 'event',
20
+ title: 'Event',
21
+ sticky: true,
22
+ renderer: (login) => (
23
+ <Badge variant={login.isRegistration ? 'default' : 'outline'}>
24
+ {login.isRegistration ? 'Registered' : 'Login'}
25
+ </Badge>
26
+ ),
27
+ },
28
+ {
29
+ name: 'device',
30
+ title: 'Device',
31
+ renderer: (login) => <span className="text-sm">{login.device}</span>,
32
+ },
33
+ {
34
+ name: 'location',
35
+ title: 'Location',
36
+ renderer: (login) => <span className="text-sm">{login.location}</span>,
37
+ },
38
+ {
39
+ name: 'referrerUrl',
40
+ title: 'Landing URL',
41
+ renderer: (login) => (
42
+ <span className="text-sm max-w-xs truncate block">{login.referrerUrl || 'N/A'}</span>
43
+ ),
44
+ },
45
+ {
46
+ name: 'ipAddress',
47
+ title: 'IP Address',
48
+ renderer: (login) => <span className="text-sm font-mono">{login.ipAddress || 'N/A'}</span>,
49
+ },
50
+ {
51
+ name: 'createdAt',
52
+ title: 'Time',
53
+ renderer: (login) => (
54
+ <span className="text-sm text-gray-600 dark:text-gray-400">
55
+ {formatDateTime(login.createdAt)}
56
+ </span>
57
+ ),
58
+ },
59
+ ]
60
+
61
+ export default function AdminUserLoginInfoTable({ rows }: { rows: LoginInfoRow[] }) {
62
+ return <DataTable columns={columns} data={rows} getRowId={(login) => login.id} />
63
+ }