create-blitzpack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +452 -0
- package/package.json +57 -0
- package/template/.dockerignore +59 -0
- package/template/.github/workflows/ci.yml +157 -0
- package/template/.husky/pre-commit +1 -0
- package/template/.husky/pre-push +1 -0
- package/template/.lintstagedrc.cjs +4 -0
- package/template/.nvmrc +1 -0
- package/template/.prettierrc +9 -0
- package/template/.vscode/settings.json +13 -0
- package/template/CLAUDE.md +175 -0
- package/template/CONTRIBUTING.md +32 -0
- package/template/Dockerfile +90 -0
- package/template/GETTING_STARTED.md +35 -0
- package/template/LICENSE +21 -0
- package/template/README.md +116 -0
- package/template/apps/api/.dockerignore +51 -0
- package/template/apps/api/.env.local.example +62 -0
- package/template/apps/api/emails/account-deleted-email.tsx +69 -0
- package/template/apps/api/emails/components/email-layout.tsx +154 -0
- package/template/apps/api/emails/config.ts +22 -0
- package/template/apps/api/emails/password-changed-email.tsx +88 -0
- package/template/apps/api/emails/password-reset-email.tsx +86 -0
- package/template/apps/api/emails/verification-email.tsx +85 -0
- package/template/apps/api/emails/welcome-email.tsx +70 -0
- package/template/apps/api/package.json +84 -0
- package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +13 -0
- package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +67 -0
- package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +5 -0
- package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +7 -0
- package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +26 -0
- package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +10 -0
- package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +29 -0
- package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +8 -0
- package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +11 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/schema.prisma +116 -0
- package/template/apps/api/prisma/seed.ts +159 -0
- package/template/apps/api/prisma.config.ts +14 -0
- package/template/apps/api/src/app.ts +377 -0
- package/template/apps/api/src/common/logger.service.ts +227 -0
- package/template/apps/api/src/config/env.ts +60 -0
- package/template/apps/api/src/config/rate-limit.ts +29 -0
- package/template/apps/api/src/hooks/auth.ts +122 -0
- package/template/apps/api/src/plugins/auth.ts +198 -0
- package/template/apps/api/src/plugins/database.ts +45 -0
- package/template/apps/api/src/plugins/logger.ts +33 -0
- package/template/apps/api/src/plugins/multipart.ts +16 -0
- package/template/apps/api/src/plugins/scalar.ts +20 -0
- package/template/apps/api/src/plugins/schedule.ts +52 -0
- package/template/apps/api/src/plugins/services.ts +66 -0
- package/template/apps/api/src/plugins/swagger.ts +56 -0
- package/template/apps/api/src/routes/accounts.ts +91 -0
- package/template/apps/api/src/routes/admin-sessions.ts +92 -0
- package/template/apps/api/src/routes/metrics.ts +71 -0
- package/template/apps/api/src/routes/password.ts +46 -0
- package/template/apps/api/src/routes/sessions.ts +53 -0
- package/template/apps/api/src/routes/stats.ts +38 -0
- package/template/apps/api/src/routes/uploads-serve.ts +27 -0
- package/template/apps/api/src/routes/uploads.ts +154 -0
- package/template/apps/api/src/routes/users.ts +114 -0
- package/template/apps/api/src/routes/verification.ts +90 -0
- package/template/apps/api/src/server.ts +34 -0
- package/template/apps/api/src/services/accounts.service.ts +125 -0
- package/template/apps/api/src/services/authorization.service.ts +162 -0
- package/template/apps/api/src/services/email.service.ts +170 -0
- package/template/apps/api/src/services/file-storage.service.ts +267 -0
- package/template/apps/api/src/services/metrics.service.ts +175 -0
- package/template/apps/api/src/services/password.service.ts +56 -0
- package/template/apps/api/src/services/sessions.service.spec.ts +134 -0
- package/template/apps/api/src/services/sessions.service.ts +276 -0
- package/template/apps/api/src/services/stats.service.ts +273 -0
- package/template/apps/api/src/services/uploads.service.ts +163 -0
- package/template/apps/api/src/services/users.service.spec.ts +249 -0
- package/template/apps/api/src/services/users.service.ts +198 -0
- package/template/apps/api/src/utils/file-validation.ts +108 -0
- package/template/apps/api/start.sh +33 -0
- package/template/apps/api/test/helpers/fastify-app.ts +24 -0
- package/template/apps/api/test/helpers/mock-authorization.ts +16 -0
- package/template/apps/api/test/helpers/mock-logger.ts +28 -0
- package/template/apps/api/test/helpers/mock-prisma.ts +30 -0
- package/template/apps/api/test/helpers/test-db.ts +125 -0
- package/template/apps/api/test/integration/auth-flow.integration.spec.ts +449 -0
- package/template/apps/api/test/integration/password.integration.spec.ts +427 -0
- package/template/apps/api/test/integration/rate-limit.integration.spec.ts +51 -0
- package/template/apps/api/test/integration/sessions.integration.spec.ts +445 -0
- package/template/apps/api/test/integration/users.integration.spec.ts +211 -0
- package/template/apps/api/test/setup.ts +31 -0
- package/template/apps/api/tsconfig.json +26 -0
- package/template/apps/api/vitest.config.ts +35 -0
- package/template/apps/web/.env.local.example +11 -0
- package/template/apps/web/components.json +24 -0
- package/template/apps/web/next.config.ts +22 -0
- package/template/apps/web/package.json +56 -0
- package/template/apps/web/postcss.config.js +5 -0
- package/template/apps/web/public/apple-icon.png +0 -0
- package/template/apps/web/public/icon.png +0 -0
- package/template/apps/web/public/robots.txt +3 -0
- package/template/apps/web/src/app/(admin)/admin/layout.tsx +222 -0
- package/template/apps/web/src/app/(admin)/admin/page.tsx +157 -0
- package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +18 -0
- package/template/apps/web/src/app/(admin)/admin/users/page.tsx +20 -0
- package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +177 -0
- package/template/apps/web/src/app/(auth)/login/page.tsx +159 -0
- package/template/apps/web/src/app/(auth)/reset-password/page.tsx +245 -0
- package/template/apps/web/src/app/(auth)/signup/page.tsx +153 -0
- package/template/apps/web/src/app/dashboard/change-password/page.tsx +255 -0
- package/template/apps/web/src/app/dashboard/page.tsx +296 -0
- package/template/apps/web/src/app/error.tsx +32 -0
- package/template/apps/web/src/app/examples/file-upload/page.tsx +200 -0
- package/template/apps/web/src/app/favicon.ico +0 -0
- package/template/apps/web/src/app/global-error.tsx +96 -0
- package/template/apps/web/src/app/globals.css +22 -0
- package/template/apps/web/src/app/icon.png +0 -0
- package/template/apps/web/src/app/layout.tsx +34 -0
- package/template/apps/web/src/app/not-found.tsx +28 -0
- package/template/apps/web/src/app/page.tsx +192 -0
- package/template/apps/web/src/components/admin/activity-feed.tsx +101 -0
- package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +114 -0
- package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +124 -0
- package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +511 -0
- package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +102 -0
- package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +90 -0
- package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +108 -0
- package/template/apps/web/src/components/admin/health-indicator.tsx +175 -0
- package/template/apps/web/src/components/admin/refresh-control.tsx +90 -0
- package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +79 -0
- package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +74 -0
- package/template/apps/web/src/components/admin/sessions-management-table.tsx +372 -0
- package/template/apps/web/src/components/admin/stat-card.tsx +137 -0
- package/template/apps/web/src/components/admin/user-create-dialog.tsx +152 -0
- package/template/apps/web/src/components/admin/user-delete-dialog.tsx +73 -0
- package/template/apps/web/src/components/admin/user-edit-dialog.tsx +170 -0
- package/template/apps/web/src/components/admin/users-management-table.tsx +285 -0
- package/template/apps/web/src/components/auth/email-verification-banner.tsx +85 -0
- package/template/apps/web/src/components/auth/github-button.tsx +40 -0
- package/template/apps/web/src/components/auth/google-button.tsx +54 -0
- package/template/apps/web/src/components/auth/protected-route.tsx +66 -0
- package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +31 -0
- package/template/apps/web/src/components/auth/with-auth.tsx +30 -0
- package/template/apps/web/src/components/error/error-card.tsx +47 -0
- package/template/apps/web/src/components/error/forbidden.tsx +25 -0
- package/template/apps/web/src/components/landing/command-block.tsx +64 -0
- package/template/apps/web/src/components/landing/feature-card.tsx +60 -0
- package/template/apps/web/src/components/landing/included-feature-card.tsx +63 -0
- package/template/apps/web/src/components/landing/logo.tsx +41 -0
- package/template/apps/web/src/components/landing/tech-badge.tsx +11 -0
- package/template/apps/web/src/components/layout/auth-nav.tsx +58 -0
- package/template/apps/web/src/components/layout/footer.tsx +3 -0
- package/template/apps/web/src/config/landing-data.ts +152 -0
- package/template/apps/web/src/config/site.ts +5 -0
- package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +181 -0
- package/template/apps/web/src/hooks/api/use-admin-sessions.ts +75 -0
- package/template/apps/web/src/hooks/api/use-admin-stats.ts +33 -0
- package/template/apps/web/src/hooks/api/use-sessions.ts +52 -0
- package/template/apps/web/src/hooks/api/use-uploads.ts +156 -0
- package/template/apps/web/src/hooks/api/use-users.ts +149 -0
- package/template/apps/web/src/hooks/use-mobile.ts +21 -0
- package/template/apps/web/src/hooks/use-realtime-metrics.ts +120 -0
- package/template/apps/web/src/lib/__tests__/utils.test.ts +29 -0
- package/template/apps/web/src/lib/api.ts +151 -0
- package/template/apps/web/src/lib/auth.ts +13 -0
- package/template/apps/web/src/lib/env.ts +52 -0
- package/template/apps/web/src/lib/form-utils.ts +11 -0
- package/template/apps/web/src/lib/utils.ts +1 -0
- package/template/apps/web/src/providers.tsx +34 -0
- package/template/apps/web/src/store/atoms.ts +15 -0
- package/template/apps/web/src/test/helpers/test-utils.tsx +44 -0
- package/template/apps/web/src/test/setup.ts +8 -0
- package/template/apps/web/tailwind.config.ts +5 -0
- package/template/apps/web/tsconfig.json +26 -0
- package/template/apps/web/vitest.config.ts +32 -0
- package/template/assets/logo-512.png +0 -0
- package/template/assets/logo.svg +4 -0
- package/template/docker-compose.prod.yml +66 -0
- package/template/docker-compose.yml +36 -0
- package/template/eslint.config.ts +119 -0
- package/template/package.json +77 -0
- package/template/packages/tailwind-config/package.json +9 -0
- package/template/packages/tailwind-config/theme.css +179 -0
- package/template/packages/types/package.json +29 -0
- package/template/packages/types/src/__tests__/schemas.test.ts +255 -0
- package/template/packages/types/src/api-response.ts +53 -0
- package/template/packages/types/src/health-check.ts +11 -0
- package/template/packages/types/src/pagination.ts +41 -0
- package/template/packages/types/src/role.ts +5 -0
- package/template/packages/types/src/session.ts +48 -0
- package/template/packages/types/src/stats.ts +113 -0
- package/template/packages/types/src/upload.ts +51 -0
- package/template/packages/types/src/user.ts +36 -0
- package/template/packages/types/tsconfig.json +5 -0
- package/template/packages/types/vitest.config.ts +21 -0
- package/template/packages/ui/components.json +21 -0
- package/template/packages/ui/package.json +108 -0
- package/template/packages/ui/src/__tests__/button.test.tsx +70 -0
- package/template/packages/ui/src/alert-dialog.tsx +141 -0
- package/template/packages/ui/src/alert.tsx +66 -0
- package/template/packages/ui/src/animated-theme-toggler.tsx +167 -0
- package/template/packages/ui/src/avatar.tsx +53 -0
- package/template/packages/ui/src/badge.tsx +36 -0
- package/template/packages/ui/src/button.tsx +84 -0
- package/template/packages/ui/src/card.tsx +92 -0
- package/template/packages/ui/src/checkbox.tsx +32 -0
- package/template/packages/ui/src/data-table/data-table-column-header.tsx +68 -0
- package/template/packages/ui/src/data-table/data-table-pagination.tsx +99 -0
- package/template/packages/ui/src/data-table/data-table-toolbar.tsx +55 -0
- package/template/packages/ui/src/data-table/data-table-view-options.tsx +63 -0
- package/template/packages/ui/src/data-table/data-table.tsx +167 -0
- package/template/packages/ui/src/dialog.tsx +143 -0
- package/template/packages/ui/src/dropdown-menu.tsx +257 -0
- package/template/packages/ui/src/empty-state.tsx +52 -0
- package/template/packages/ui/src/file-upload-input.tsx +202 -0
- package/template/packages/ui/src/form.tsx +168 -0
- package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
- package/template/packages/ui/src/icons/brand-icons.tsx +16 -0
- package/template/packages/ui/src/input.tsx +21 -0
- package/template/packages/ui/src/label.tsx +24 -0
- package/template/packages/ui/src/lib/utils.ts +6 -0
- package/template/packages/ui/src/password-input.tsx +102 -0
- package/template/packages/ui/src/popover.tsx +48 -0
- package/template/packages/ui/src/radio-group.tsx +45 -0
- package/template/packages/ui/src/scroll-area.tsx +58 -0
- package/template/packages/ui/src/select.tsx +187 -0
- package/template/packages/ui/src/separator.tsx +28 -0
- package/template/packages/ui/src/sheet.tsx +139 -0
- package/template/packages/ui/src/sidebar.tsx +726 -0
- package/template/packages/ui/src/skeleton-variants.tsx +87 -0
- package/template/packages/ui/src/skeleton.tsx +13 -0
- package/template/packages/ui/src/slider.tsx +63 -0
- package/template/packages/ui/src/sonner.tsx +25 -0
- package/template/packages/ui/src/spinner.tsx +16 -0
- package/template/packages/ui/src/switch.tsx +31 -0
- package/template/packages/ui/src/table.tsx +116 -0
- package/template/packages/ui/src/tabs.tsx +66 -0
- package/template/packages/ui/src/textarea.tsx +18 -0
- package/template/packages/ui/src/tooltip.tsx +61 -0
- package/template/packages/ui/src/user-avatar.tsx +97 -0
- package/template/packages/ui/test-config.js +3 -0
- package/template/packages/ui/tsconfig.json +12 -0
- package/template/packages/ui/turbo.json +18 -0
- package/template/packages/ui/vitest.config.ts +17 -0
- package/template/packages/ui/vitest.setup.ts +1 -0
- package/template/packages/utils/package.json +23 -0
- package/template/packages/utils/src/__tests__/utils.test.ts +223 -0
- package/template/packages/utils/src/array.ts +18 -0
- package/template/packages/utils/src/async.ts +3 -0
- package/template/packages/utils/src/date.ts +77 -0
- package/template/packages/utils/src/errors.ts +73 -0
- package/template/packages/utils/src/number.ts +11 -0
- package/template/packages/utils/src/string.ts +13 -0
- package/template/packages/utils/tsconfig.json +5 -0
- package/template/packages/utils/vitest.config.ts +21 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/tsconfig.base.json +32 -0
- package/template/turbo.json +133 -0
- package/template/vitest.shared.ts +26 -0
- package/template/vitest.workspace.ts +9 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { UserGrowthPoint } from '@repo/packages-types/stats';
|
|
4
|
+
import { Skeleton } from '@repo/packages-ui/skeleton';
|
|
5
|
+
import {
|
|
6
|
+
Area,
|
|
7
|
+
AreaChart,
|
|
8
|
+
CartesianGrid,
|
|
9
|
+
ResponsiveContainer,
|
|
10
|
+
Tooltip,
|
|
11
|
+
XAxis,
|
|
12
|
+
YAxis,
|
|
13
|
+
} from 'recharts';
|
|
14
|
+
|
|
15
|
+
import { ChartTooltip } from './chart-tooltip';
|
|
16
|
+
|
|
17
|
+
interface UserGrowthChartProps {
|
|
18
|
+
data: UserGrowthPoint[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatDate(dateStr: string) {
|
|
22
|
+
const date = new Date(dateStr);
|
|
23
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function UserGrowthChart({ data }: UserGrowthChartProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="bg-card rounded-xl border p-5">
|
|
29
|
+
<div className="mb-4">
|
|
30
|
+
<h3 className="text-sm font-semibold">User Growth</h3>
|
|
31
|
+
<p className="text-muted-foreground text-xs">
|
|
32
|
+
New signups and cumulative users (30 days)
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
<div className="h-[240px]">
|
|
36
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
37
|
+
<AreaChart
|
|
38
|
+
data={data}
|
|
39
|
+
margin={{ top: 5, right: 5, left: -20, bottom: 0 }}
|
|
40
|
+
>
|
|
41
|
+
<defs>
|
|
42
|
+
<linearGradient id="colorCumulative" x1="0" y1="0" x2="0" y2="1">
|
|
43
|
+
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
|
|
44
|
+
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
|
|
45
|
+
</linearGradient>
|
|
46
|
+
<linearGradient id="colorNew" x1="0" y1="0" x2="0" y2="1">
|
|
47
|
+
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
|
|
48
|
+
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
|
|
49
|
+
</linearGradient>
|
|
50
|
+
</defs>
|
|
51
|
+
<CartesianGrid
|
|
52
|
+
strokeDasharray="3 3"
|
|
53
|
+
vertical={false}
|
|
54
|
+
className="stroke-zinc-200 dark:stroke-zinc-700"
|
|
55
|
+
/>
|
|
56
|
+
<XAxis
|
|
57
|
+
dataKey="date"
|
|
58
|
+
tickFormatter={formatDate}
|
|
59
|
+
tick={{ fontSize: 11 }}
|
|
60
|
+
tickLine={false}
|
|
61
|
+
axisLine={false}
|
|
62
|
+
interval="preserveStartEnd"
|
|
63
|
+
className="text-zinc-500 dark:text-zinc-400"
|
|
64
|
+
/>
|
|
65
|
+
<YAxis
|
|
66
|
+
tick={{ fontSize: 11 }}
|
|
67
|
+
tickLine={false}
|
|
68
|
+
axisLine={false}
|
|
69
|
+
className="text-zinc-500 dark:text-zinc-400"
|
|
70
|
+
/>
|
|
71
|
+
<Tooltip
|
|
72
|
+
content={<ChartTooltip labelFormatter={formatDate} />}
|
|
73
|
+
cursor={{ stroke: 'rgba(99, 102, 241, 0.3)', strokeWidth: 1 }}
|
|
74
|
+
/>
|
|
75
|
+
<Area
|
|
76
|
+
type="monotone"
|
|
77
|
+
dataKey="cumulative"
|
|
78
|
+
name="Total Users"
|
|
79
|
+
stroke="#6366f1"
|
|
80
|
+
strokeWidth={2}
|
|
81
|
+
fill="url(#colorCumulative)"
|
|
82
|
+
/>
|
|
83
|
+
<Area
|
|
84
|
+
type="monotone"
|
|
85
|
+
dataKey="count"
|
|
86
|
+
name="New Users"
|
|
87
|
+
stroke="#10b981"
|
|
88
|
+
strokeWidth={2}
|
|
89
|
+
fill="url(#colorNew)"
|
|
90
|
+
/>
|
|
91
|
+
</AreaChart>
|
|
92
|
+
</ResponsiveContainer>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function UserGrowthChartSkeleton() {
|
|
99
|
+
return (
|
|
100
|
+
<div className="bg-card rounded-xl border p-5">
|
|
101
|
+
<div className="mb-4 space-y-1">
|
|
102
|
+
<Skeleton className="h-4 w-24" />
|
|
103
|
+
<Skeleton className="h-3 w-48" />
|
|
104
|
+
</div>
|
|
105
|
+
<Skeleton className="h-[240px] w-full" />
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { SystemHealth } from '@repo/packages-types/stats';
|
|
4
|
+
import { cn } from '@repo/packages-ui/lib/utils';
|
|
5
|
+
import { Skeleton } from '@repo/packages-ui/skeleton';
|
|
6
|
+
import { motion } from 'framer-motion';
|
|
7
|
+
import { Activity, Clock, Database } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
interface HealthIndicatorProps {
|
|
10
|
+
health: SystemHealth;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatUptime(seconds: number) {
|
|
14
|
+
const days = Math.floor(seconds / 86400);
|
|
15
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
16
|
+
const mins = Math.floor((seconds % 3600) / 60);
|
|
17
|
+
|
|
18
|
+
if (days > 0) return `${days}d ${hours}h`;
|
|
19
|
+
if (hours > 0) return `${hours}h ${mins}m`;
|
|
20
|
+
return `${mins}m`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const STATUS_CONFIG = {
|
|
24
|
+
connected: {
|
|
25
|
+
label: 'Healthy',
|
|
26
|
+
color: 'bg-emerald-500',
|
|
27
|
+
textColor: 'text-emerald-600 dark:text-emerald-400',
|
|
28
|
+
bgColor: 'bg-emerald-500/10',
|
|
29
|
+
},
|
|
30
|
+
degraded: {
|
|
31
|
+
label: 'Degraded',
|
|
32
|
+
color: 'bg-amber-500',
|
|
33
|
+
textColor: 'text-amber-600 dark:text-amber-400',
|
|
34
|
+
bgColor: 'bg-amber-500/10',
|
|
35
|
+
},
|
|
36
|
+
disconnected: {
|
|
37
|
+
label: 'Offline',
|
|
38
|
+
color: 'bg-rose-500',
|
|
39
|
+
textColor: 'text-rose-600 dark:text-rose-400',
|
|
40
|
+
bgColor: 'bg-rose-500/10',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function PulseDot({
|
|
45
|
+
status,
|
|
46
|
+
}: {
|
|
47
|
+
status: 'connected' | 'degraded' | 'disconnected';
|
|
48
|
+
}) {
|
|
49
|
+
const config = STATUS_CONFIG[status];
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<span className="relative flex size-3">
|
|
53
|
+
<motion.span
|
|
54
|
+
className={cn(
|
|
55
|
+
'absolute inline-flex h-full w-full rounded-full opacity-75',
|
|
56
|
+
config.color
|
|
57
|
+
)}
|
|
58
|
+
animate={{ scale: [1, 1.5, 1], opacity: [0.75, 0, 0.75] }}
|
|
59
|
+
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
|
|
60
|
+
/>
|
|
61
|
+
<span
|
|
62
|
+
className={cn('relative inline-flex size-3 rounded-full', config.color)}
|
|
63
|
+
/>
|
|
64
|
+
</span>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function HealthIndicator({ health }: HealthIndicatorProps) {
|
|
69
|
+
const dbStatus = STATUS_CONFIG[health.database];
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="bg-card rounded-xl border p-5">
|
|
73
|
+
<div className="mb-4 flex items-center justify-between">
|
|
74
|
+
<div>
|
|
75
|
+
<h3 className="text-sm font-semibold">System Health</h3>
|
|
76
|
+
<p className="text-muted-foreground text-xs">Real-time status</p>
|
|
77
|
+
</div>
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
'flex items-center gap-2 rounded-full px-3 py-1',
|
|
81
|
+
dbStatus.bgColor
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<PulseDot status={health.database} />
|
|
85
|
+
<span className={cn('text-xs font-medium', dbStatus.textColor)}>
|
|
86
|
+
{dbStatus.label}
|
|
87
|
+
</span>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="space-y-4">
|
|
92
|
+
<div className="flex items-center justify-between rounded-lg border p-3">
|
|
93
|
+
<div className="flex items-center gap-3">
|
|
94
|
+
<div className="rounded-lg bg-blue-500/10 p-2">
|
|
95
|
+
<Database className="size-4 text-blue-500" />
|
|
96
|
+
</div>
|
|
97
|
+
<div>
|
|
98
|
+
<p className="text-sm font-medium">Database</p>
|
|
99
|
+
<p className="text-muted-foreground text-xs">PostgreSQL</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="text-right">
|
|
103
|
+
<p className={cn('text-sm font-medium', dbStatus.textColor)}>
|
|
104
|
+
{dbStatus.label}
|
|
105
|
+
</p>
|
|
106
|
+
{health.dbLatencyMs !== undefined && (
|
|
107
|
+
<p className="text-muted-foreground text-xs">
|
|
108
|
+
{health.dbLatencyMs}ms latency
|
|
109
|
+
</p>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="flex items-center justify-between rounded-lg border p-3">
|
|
115
|
+
<div className="flex items-center gap-3">
|
|
116
|
+
<div className="rounded-lg bg-violet-500/10 p-2">
|
|
117
|
+
<Clock className="size-4 text-violet-500" />
|
|
118
|
+
</div>
|
|
119
|
+
<div>
|
|
120
|
+
<p className="text-sm font-medium">Uptime</p>
|
|
121
|
+
<p className="text-muted-foreground text-xs">API Server</p>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<p className="text-sm font-medium">{formatUptime(health.uptime)}</p>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="flex items-center justify-between rounded-lg border p-3">
|
|
128
|
+
<div className="flex items-center gap-3">
|
|
129
|
+
<div className="rounded-lg bg-emerald-500/10 p-2">
|
|
130
|
+
<Activity className="size-4 text-emerald-500" />
|
|
131
|
+
</div>
|
|
132
|
+
<div>
|
|
133
|
+
<p className="text-sm font-medium">Last Check</p>
|
|
134
|
+
<p className="text-muted-foreground text-xs">Health check</p>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
<p className="text-muted-foreground text-xs">
|
|
138
|
+
{new Date(health.lastChecked).toLocaleTimeString()}
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function HealthIndicatorSkeleton() {
|
|
147
|
+
return (
|
|
148
|
+
<div className="bg-card rounded-xl border p-5">
|
|
149
|
+
<div className="mb-4 flex items-center justify-between">
|
|
150
|
+
<div className="space-y-1">
|
|
151
|
+
<Skeleton className="h-4 w-24" />
|
|
152
|
+
<Skeleton className="h-3 w-20" />
|
|
153
|
+
</div>
|
|
154
|
+
<Skeleton className="h-6 w-20 rounded-full" />
|
|
155
|
+
</div>
|
|
156
|
+
<div className="space-y-4">
|
|
157
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
158
|
+
<div
|
|
159
|
+
key={i}
|
|
160
|
+
className="flex items-center justify-between rounded-lg border p-3"
|
|
161
|
+
>
|
|
162
|
+
<div className="flex items-center gap-3">
|
|
163
|
+
<Skeleton className="size-8 rounded-lg" />
|
|
164
|
+
<div className="space-y-1">
|
|
165
|
+
<Skeleton className="h-3 w-16" />
|
|
166
|
+
<Skeleton className="h-2 w-12" />
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<Skeleton className="h-4 w-14" />
|
|
170
|
+
</div>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@repo/packages-ui/button';
|
|
4
|
+
import {
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue,
|
|
10
|
+
} from '@repo/packages-ui/select';
|
|
11
|
+
import { motion } from 'framer-motion';
|
|
12
|
+
import { RefreshCw } from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
interface RefreshControlProps {
|
|
15
|
+
interval: number | null;
|
|
16
|
+
onIntervalChange: (interval: number | null) => void;
|
|
17
|
+
onRefresh: () => void;
|
|
18
|
+
isRefreshing: boolean;
|
|
19
|
+
lastUpdated: Date | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const INTERVALS = [
|
|
23
|
+
{ value: '5000', label: '5s' },
|
|
24
|
+
{ value: '15000', label: '15s' },
|
|
25
|
+
{ value: '30000', label: '30s' },
|
|
26
|
+
{ value: '60000', label: '1m' },
|
|
27
|
+
{ value: 'off', label: 'Off' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function RefreshControl({
|
|
31
|
+
interval,
|
|
32
|
+
onIntervalChange,
|
|
33
|
+
onRefresh,
|
|
34
|
+
isRefreshing,
|
|
35
|
+
lastUpdated,
|
|
36
|
+
}: RefreshControlProps) {
|
|
37
|
+
const handleIntervalChange = (value: string) => {
|
|
38
|
+
if (value === 'off') {
|
|
39
|
+
onIntervalChange(null);
|
|
40
|
+
} else {
|
|
41
|
+
onIntervalChange(parseInt(value, 10));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex items-center gap-3">
|
|
47
|
+
{lastUpdated && (
|
|
48
|
+
<span className="text-muted-foreground text-xs">
|
|
49
|
+
Updated {lastUpdated.toLocaleTimeString()}
|
|
50
|
+
</span>
|
|
51
|
+
)}
|
|
52
|
+
<div className="flex items-center gap-2">
|
|
53
|
+
<Select
|
|
54
|
+
value={interval?.toString() ?? 'off'}
|
|
55
|
+
onValueChange={handleIntervalChange}
|
|
56
|
+
>
|
|
57
|
+
<SelectTrigger className="h-8 w-[80px] text-xs">
|
|
58
|
+
<SelectValue placeholder="Auto" />
|
|
59
|
+
</SelectTrigger>
|
|
60
|
+
<SelectContent>
|
|
61
|
+
{INTERVALS.map((opt) => (
|
|
62
|
+
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
|
63
|
+
{opt.label}
|
|
64
|
+
</SelectItem>
|
|
65
|
+
))}
|
|
66
|
+
</SelectContent>
|
|
67
|
+
</Select>
|
|
68
|
+
<Button
|
|
69
|
+
variant="outline"
|
|
70
|
+
size="sm"
|
|
71
|
+
onClick={onRefresh}
|
|
72
|
+
disabled={isRefreshing}
|
|
73
|
+
className="h-8 gap-1.5 px-2.5"
|
|
74
|
+
>
|
|
75
|
+
<motion.div
|
|
76
|
+
animate={isRefreshing ? { rotate: 360 } : { rotate: 0 }}
|
|
77
|
+
transition={
|
|
78
|
+
isRefreshing
|
|
79
|
+
? { duration: 1, repeat: Infinity, ease: 'linear' }
|
|
80
|
+
: { duration: 0 }
|
|
81
|
+
}
|
|
82
|
+
>
|
|
83
|
+
<RefreshCw className="size-3.5" />
|
|
84
|
+
</motion.div>
|
|
85
|
+
<span className="text-xs">Refresh</span>
|
|
86
|
+
</Button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AlertDialog,
|
|
5
|
+
AlertDialogAction,
|
|
6
|
+
AlertDialogCancel,
|
|
7
|
+
AlertDialogContent,
|
|
8
|
+
AlertDialogDescription,
|
|
9
|
+
AlertDialogFooter,
|
|
10
|
+
AlertDialogHeader,
|
|
11
|
+
AlertDialogTitle,
|
|
12
|
+
AlertDialogTrigger,
|
|
13
|
+
} from '@repo/packages-ui/alert-dialog';
|
|
14
|
+
import { Button } from '@repo/packages-ui/button';
|
|
15
|
+
import { LogOut } from 'lucide-react';
|
|
16
|
+
import * as React from 'react';
|
|
17
|
+
import { toast } from 'sonner';
|
|
18
|
+
|
|
19
|
+
import { useRevokeUserSessions } from '@/hooks/api/use-admin-sessions';
|
|
20
|
+
|
|
21
|
+
interface SessionRevokeAllDialogProps {
|
|
22
|
+
userId: string;
|
|
23
|
+
userName: string;
|
|
24
|
+
userEmail: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function SessionRevokeAllDialog({
|
|
28
|
+
userId,
|
|
29
|
+
userName,
|
|
30
|
+
userEmail,
|
|
31
|
+
}: SessionRevokeAllDialogProps) {
|
|
32
|
+
const [open, setOpen] = React.useState(false);
|
|
33
|
+
const { mutate: revokeUserSessions, isPending } = useRevokeUserSessions();
|
|
34
|
+
|
|
35
|
+
const handleRevoke = () => {
|
|
36
|
+
revokeUserSessions(userId, {
|
|
37
|
+
onSuccess: () => {
|
|
38
|
+
toast.success('All sessions revoked successfully');
|
|
39
|
+
setOpen(false);
|
|
40
|
+
},
|
|
41
|
+
onError: (error) => {
|
|
42
|
+
toast.error(error.message || 'Failed to revoke sessions');
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
49
|
+
<AlertDialogTrigger asChild>
|
|
50
|
+
<Button variant="outline" size="sm" className="text-destructive">
|
|
51
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
52
|
+
Revoke All
|
|
53
|
+
</Button>
|
|
54
|
+
</AlertDialogTrigger>
|
|
55
|
+
<AlertDialogContent>
|
|
56
|
+
<AlertDialogHeader>
|
|
57
|
+
<AlertDialogTitle>
|
|
58
|
+
Revoke all sessions for this user?
|
|
59
|
+
</AlertDialogTitle>
|
|
60
|
+
<AlertDialogDescription>
|
|
61
|
+
This will immediately log out{' '}
|
|
62
|
+
<span className="font-semibold">{userName || userEmail}</span> from
|
|
63
|
+
all devices. They will need to sign in again on each device.
|
|
64
|
+
</AlertDialogDescription>
|
|
65
|
+
</AlertDialogHeader>
|
|
66
|
+
<AlertDialogFooter>
|
|
67
|
+
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
|
68
|
+
<AlertDialogAction
|
|
69
|
+
onClick={handleRevoke}
|
|
70
|
+
disabled={isPending}
|
|
71
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
72
|
+
>
|
|
73
|
+
{isPending ? 'Revoking...' : 'Revoke All Sessions'}
|
|
74
|
+
</AlertDialogAction>
|
|
75
|
+
</AlertDialogFooter>
|
|
76
|
+
</AlertDialogContent>
|
|
77
|
+
</AlertDialog>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { AdminSession } from '@repo/packages-types/session';
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogFooter,
|
|
11
|
+
AlertDialogHeader,
|
|
12
|
+
AlertDialogTitle,
|
|
13
|
+
AlertDialogTrigger,
|
|
14
|
+
} from '@repo/packages-ui/alert-dialog';
|
|
15
|
+
import { Button } from '@repo/packages-ui/button';
|
|
16
|
+
import { LogOut } from 'lucide-react';
|
|
17
|
+
import * as React from 'react';
|
|
18
|
+
import { toast } from 'sonner';
|
|
19
|
+
|
|
20
|
+
import { useRevokeSession } from '@/hooks/api/use-admin-sessions';
|
|
21
|
+
|
|
22
|
+
interface SessionRevokeDialogProps {
|
|
23
|
+
session: AdminSession;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SessionRevokeDialog({ session }: SessionRevokeDialogProps) {
|
|
27
|
+
const [open, setOpen] = React.useState(false);
|
|
28
|
+
const { mutate: revokeSession, isPending } = useRevokeSession();
|
|
29
|
+
|
|
30
|
+
const handleRevoke = () => {
|
|
31
|
+
revokeSession(session.id, {
|
|
32
|
+
onSuccess: () => {
|
|
33
|
+
toast.success('Session revoked successfully');
|
|
34
|
+
setOpen(false);
|
|
35
|
+
},
|
|
36
|
+
onError: (error) => {
|
|
37
|
+
toast.error(error.message || 'Failed to revoke session');
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
44
|
+
<AlertDialogTrigger asChild>
|
|
45
|
+
<Button variant="ghost" size="sm" className="text-destructive">
|
|
46
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
47
|
+
Revoke
|
|
48
|
+
</Button>
|
|
49
|
+
</AlertDialogTrigger>
|
|
50
|
+
<AlertDialogContent>
|
|
51
|
+
<AlertDialogHeader>
|
|
52
|
+
<AlertDialogTitle>Revoke this session?</AlertDialogTitle>
|
|
53
|
+
<AlertDialogDescription>
|
|
54
|
+
This will immediately log out{' '}
|
|
55
|
+
<span className="font-semibold">
|
|
56
|
+
{session.user.name || session.user.email}
|
|
57
|
+
</span>{' '}
|
|
58
|
+
from this device. They will need to sign in again to continue.
|
|
59
|
+
</AlertDialogDescription>
|
|
60
|
+
</AlertDialogHeader>
|
|
61
|
+
<AlertDialogFooter>
|
|
62
|
+
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
|
63
|
+
<AlertDialogAction
|
|
64
|
+
onClick={handleRevoke}
|
|
65
|
+
disabled={isPending}
|
|
66
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
67
|
+
>
|
|
68
|
+
{isPending ? 'Revoking...' : 'Revoke Session'}
|
|
69
|
+
</AlertDialogAction>
|
|
70
|
+
</AlertDialogFooter>
|
|
71
|
+
</AlertDialogContent>
|
|
72
|
+
</AlertDialog>
|
|
73
|
+
);
|
|
74
|
+
}
|