create-stackr 0.2.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/LICENSE +21 -0
- package/README.md +642 -0
- package/bin/cli.js +12 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +113 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/dependencies.d.ts +82 -0
- package/dist/config/dependencies.d.ts.map +1 -0
- package/dist/config/dependencies.js +82 -0
- package/dist/config/dependencies.js.map +1 -0
- package/dist/config/presets.d.ts +3 -0
- package/dist/config/presets.d.ts.map +1 -0
- package/dist/config/presets.js +174 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/generators/index.d.ts +40 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +130 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/onboarding.d.ts +8 -0
- package/dist/generators/onboarding.d.ts.map +1 -0
- package/dist/generators/onboarding.js +141 -0
- package/dist/generators/onboarding.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/features.d.ts +14 -0
- package/dist/prompts/features.d.ts.map +1 -0
- package/dist/prompts/features.js +96 -0
- package/dist/prompts/features.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +93 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/onboarding.d.ts +6 -0
- package/dist/prompts/onboarding.d.ts.map +1 -0
- package/dist/prompts/onboarding.js +37 -0
- package/dist/prompts/onboarding.js.map +1 -0
- package/dist/prompts/orm.d.ts +3 -0
- package/dist/prompts/orm.d.ts.map +1 -0
- package/dist/prompts/orm.js +23 -0
- package/dist/prompts/orm.js.map +1 -0
- package/dist/prompts/packageManager.d.ts +2 -0
- package/dist/prompts/packageManager.d.ts.map +1 -0
- package/dist/prompts/packageManager.js +18 -0
- package/dist/prompts/packageManager.js.map +1 -0
- package/dist/prompts/platform.d.ts +3 -0
- package/dist/prompts/platform.d.ts.map +1 -0
- package/dist/prompts/platform.js +21 -0
- package/dist/prompts/platform.js.map +1 -0
- package/dist/prompts/preset.d.ts +4 -0
- package/dist/prompts/preset.d.ts.map +1 -0
- package/dist/prompts/preset.js +165 -0
- package/dist/prompts/preset.js.map +1 -0
- package/dist/prompts/project.d.ts +2 -0
- package/dist/prompts/project.d.ts.map +1 -0
- package/dist/prompts/project.js +27 -0
- package/dist/prompts/project.js.map +1 -0
- package/dist/prompts/sdks.d.ts +2 -0
- package/dist/prompts/sdks.d.ts.map +1 -0
- package/dist/prompts/sdks.js +46 -0
- package/dist/prompts/sdks.js.map +1 -0
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +25 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cleanup.d.ts +5 -0
- package/dist/utils/cleanup.d.ts.map +1 -0
- package/dist/utils/cleanup.js +38 -0
- package/dist/utils/cleanup.js.map +1 -0
- package/dist/utils/copy.d.ts +10 -0
- package/dist/utils/copy.d.ts.map +1 -0
- package/dist/utils/copy.js +53 -0
- package/dist/utils/copy.js.map +1 -0
- package/dist/utils/errors.d.ts +33 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +136 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/git.d.ts +5 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +33 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +22 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/package.d.ts +16 -0
- package/dist/utils/package.d.ts.map +1 -0
- package/dist/utils/package.js +86 -0
- package/dist/utils/package.js.map +1 -0
- package/dist/utils/system-validation.d.ts +9 -0
- package/dist/utils/system-validation.d.ts.map +1 -0
- package/dist/utils/system-validation.js +31 -0
- package/dist/utils/system-validation.js.map +1 -0
- package/dist/utils/template.d.ts +20 -0
- package/dist/utils/template.d.ts.map +1 -0
- package/dist/utils/template.js +234 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +94 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +96 -0
- package/templates/base/backend/.dockerignore.ejs +62 -0
- package/templates/base/backend/.env.example.ejs +116 -0
- package/templates/base/backend/Dockerfile.ejs +142 -0
- package/templates/base/backend/controllers/event-queue/index.ts +20 -0
- package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
- package/templates/base/backend/controllers/rest-api/index.ts +48 -0
- package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
- package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
- package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
- package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
- package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
- package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
- package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
- package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
- package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
- package/templates/base/backend/domain/device-session/schema.ts +72 -0
- package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
- package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
- package/templates/base/backend/domain/session/schema.ts +29 -0
- package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
- package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
- package/templates/base/backend/domain/user/schema.ts +14 -0
- package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
- package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
- package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
- package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
- package/templates/base/backend/lib/constants.ts.ejs +29 -0
- package/templates/base/backend/package.json.ejs +50 -0
- package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
- package/templates/base/backend/prisma.config.prisma.ts +12 -0
- package/templates/base/backend/tsconfig.json +39 -0
- package/templates/base/backend/utils/db.drizzle.ts +41 -0
- package/templates/base/backend/utils/db.prisma.ts +51 -0
- package/templates/base/backend/utils/email.ts.ejs +35 -0
- package/templates/base/backend/utils/errors.ts +348 -0
- package/templates/base/backend/utils/redis.ts.ejs +279 -0
- package/templates/base/mobile/.env.example.ejs +35 -0
- package/templates/base/mobile/.gitignore.ejs +167 -0
- package/templates/base/mobile/app/+not-found.tsx +85 -0
- package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
- package/templates/base/mobile/app.json.ejs +88 -0
- package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
- package/templates/base/mobile/assets/images/favicon.png +0 -0
- package/templates/base/mobile/assets/images/icon.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
- package/templates/base/mobile/assets/images/paywall_image.png +0 -0
- package/templates/base/mobile/assets/images/splash.png +0 -0
- package/templates/base/mobile/eas.json.ejs +49 -0
- package/templates/base/mobile/metro.config.js +9 -0
- package/templates/base/mobile/package.json.ejs +53 -0
- package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
- package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
- package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
- package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
- package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
- package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
- package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
- package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
- package/templates/base/mobile/src/components/ui/index.ts +6 -0
- package/templates/base/mobile/src/constants/Theme.ts +163 -0
- package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
- package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
- package/templates/base/mobile/src/services/api.ts.ejs +71 -0
- package/templates/base/mobile/src/services/errorService.ts +179 -0
- package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
- package/templates/base/mobile/src/store/index.ts.ejs +18 -0
- package/templates/base/mobile/src/store/ui.store.ts +100 -0
- package/templates/base/mobile/src/utils/formatters.ts +105 -0
- package/templates/base/mobile/src/utils/logger.ts +73 -0
- package/templates/base/mobile/src/utils/responsive.ts +234 -0
- package/templates/base/mobile/tsconfig.json +32 -0
- package/templates/base/web/.env.example.ejs +26 -0
- package/templates/base/web/components.json +22 -0
- package/templates/base/web/eslint.config.mjs +18 -0
- package/templates/base/web/next.config.ts +7 -0
- package/templates/base/web/package.json.ejs +35 -0
- package/templates/base/web/postcss.config.mjs +7 -0
- package/templates/base/web/public/.gitkeep +0 -0
- package/templates/base/web/public/file.svg +1 -0
- package/templates/base/web/public/globe.svg +1 -0
- package/templates/base/web/public/next.svg +1 -0
- package/templates/base/web/public/vercel.svg +1 -0
- package/templates/base/web/public/window.svg +1 -0
- package/templates/base/web/src/app/favicon.ico +0 -0
- package/templates/base/web/src/app/globals.css +152 -0
- package/templates/base/web/src/app/layout.tsx.ejs +54 -0
- package/templates/base/web/src/app/page.tsx.ejs +92 -0
- package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
- package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
- package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
- package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
- package/templates/base/web/src/components/theme-toggle.tsx +34 -0
- package/templates/base/web/src/components/ui/button.tsx +62 -0
- package/templates/base/web/src/components/ui/card.tsx +92 -0
- package/templates/base/web/src/components/ui/input.tsx +21 -0
- package/templates/base/web/src/components/ui/label.tsx +24 -0
- package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/base/web/src/components/ui/spinner.tsx +20 -0
- package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
- package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
- package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
- package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
- package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
- package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
- package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
- package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
- package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
- package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
- package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
- package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
- package/templates/base/web/src/lib/utils.ts +6 -0
- package/templates/base/web/src/proxy.ts.ejs +66 -0
- package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
- package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
- package/templates/base/web/tsconfig.json +34 -0
- package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
- package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
- package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
- package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
- package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
- package/templates/features/mobile/auth/components/auth/index.ts +2 -0
- package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
- package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
- package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
- package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
- package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
- package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
- package/templates/features/web/.gitkeep +0 -0
- package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
- package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
- package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
- package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
- package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
- package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
- package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
- package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
- package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
- package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
- package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
- package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
- package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
- package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
- package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
- package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
- package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
- package/templates/integrations/mobile/att/services/attService.ts +84 -0
- package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
- package/templates/integrations/mobile/att/store/att.store.ts +162 -0
- package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
- package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
- package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
- package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
- package/templates/integrations/web/.gitkeep +0 -0
- package/templates/shared/.env.example.ejs +21 -0
- package/templates/shared/.gitignore.ejs +145 -0
- package/templates/shared/README.md.ejs +134 -0
- package/templates/shared/docker-compose.prod.yml.ejs +120 -0
- package/templates/shared/docker-compose.yml.ejs +129 -0
- package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
- package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
- package/templates/shared/scripts/setup.sh.ejs +979 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import { ThemeToggle } from "@/components/theme-toggle";
|
|
4
|
+
|
|
5
|
+
export default function Home() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="min-h-screen flex flex-col bg-background">
|
|
8
|
+
{/* Navbar */}
|
|
9
|
+
<header className="fixed top-0 w-full border-b border-border/40 bg-background/80 backdrop-blur-xl z-50">
|
|
10
|
+
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
|
11
|
+
<Link href="/" className="text-lg font-bold tracking-tight flex items-center gap-2">
|
|
12
|
+
<div className="h-6 w-6 rounded-md bg-primary flex items-center justify-center">
|
|
13
|
+
<span className="text-primary-foreground text-xs font-bold">A</span>
|
|
14
|
+
</div>
|
|
15
|
+
<%= projectName %>
|
|
16
|
+
</Link>
|
|
17
|
+
<div className="flex items-center gap-4">
|
|
18
|
+
<ThemeToggle />
|
|
19
|
+
<Link href="/login" className="hidden sm:block">
|
|
20
|
+
<Button variant="ghost" className="text-muted-foreground hover:text-foreground">Sign in</Button>
|
|
21
|
+
</Link>
|
|
22
|
+
<Link href="/register">
|
|
23
|
+
<Button className="font-medium shadow-lg shadow-primary/20">Get started</Button>
|
|
24
|
+
</Link>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</header>
|
|
28
|
+
|
|
29
|
+
{/* Hero Section */}
|
|
30
|
+
<main className="flex-1 flex flex-col">
|
|
31
|
+
<section className="relative pt-32 pb-24 lg:pt-48 lg:pb-32 overflow-hidden">
|
|
32
|
+
<div className="max-w-7xl mx-auto px-6 text-center space-y-10">
|
|
33
|
+
{/* Announcement Pill */}
|
|
34
|
+
<div className="fade-in-up delay-100 flex justify-center">
|
|
35
|
+
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-border/50 bg-background/50 backdrop-blur-sm text-sm text-muted-foreground hover:bg-muted/50 transition-colors cursor-pointer">
|
|
36
|
+
<span className="relative flex h-2 w-2">
|
|
37
|
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
|
38
|
+
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
|
39
|
+
</span>
|
|
40
|
+
<span className="font-medium">v1.0 is now live</span>
|
|
41
|
+
<span className="text-muted-foreground/50">|</span>
|
|
42
|
+
<span className="hidden sm:inline">Check out what's new →</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Headline */}
|
|
47
|
+
<div className="space-y-6 max-w-4xl mx-auto">
|
|
48
|
+
<h1 className="text-5xl sm:text-6xl lg:text-7xl font-bold tracking-tight text-foreground leading-[1.1] sm:leading-[1.1]">
|
|
49
|
+
Build something <br className="hidden sm:block" />
|
|
50
|
+
<span className="text-foreground">
|
|
51
|
+
remarkable today.
|
|
52
|
+
</span>
|
|
53
|
+
</h1>
|
|
54
|
+
|
|
55
|
+
<p className="text-lg sm:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
|
56
|
+
Ship your next big idea with authentication, premium UI components, and solid engineering practices built-in.
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* CTAs */}
|
|
61
|
+
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4">
|
|
62
|
+
<Link href="/register">
|
|
63
|
+
<Button size="lg" className="h-12 px-8 text-base shadow-xl shadow-primary/20 hover:shadow-primary/30 transition-all duration-300">
|
|
64
|
+
Start building free
|
|
65
|
+
</Button>
|
|
66
|
+
</Link>
|
|
67
|
+
<Link href="/login">
|
|
68
|
+
<Button variant="outline" size="lg" className="h-12 px-8 text-base backdrop-blur-sm hover:bg-muted/50">
|
|
69
|
+
View demo
|
|
70
|
+
</Button>
|
|
71
|
+
</Link>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
</main>
|
|
76
|
+
|
|
77
|
+
{/* Slimmer Footer */}
|
|
78
|
+
<footer className="border-t border-border/40 py-6 bg-muted/20">
|
|
79
|
+
<div className="max-w-7xl mx-auto px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
80
|
+
<p className="text-sm text-muted-foreground">
|
|
81
|
+
© {new Date().getFullYear()} <%= projectName %>.
|
|
82
|
+
</p>
|
|
83
|
+
<div className="flex gap-6 text-sm text-muted-foreground">
|
|
84
|
+
<Link href="#" className="hover:text-foreground transition-colors">Privacy</Link>
|
|
85
|
+
<Link href="#" className="hover:text-foreground transition-colors">Terms</Link>
|
|
86
|
+
<Link href="#" className="hover:text-foreground transition-colors">GitHub</Link>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</footer>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useAuthStore } from "@/store/auth.store";
|
|
5
|
+
import type { AuthSession } from "@/lib/auth/actions";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hydrates the auth store with the initial session from RSC.
|
|
9
|
+
* Renders nothing - just a side effect component.
|
|
10
|
+
*/
|
|
11
|
+
export function AuthHydrator({ session }: { session: AuthSession | null }) {
|
|
12
|
+
const hydrate = useAuthStore((s) => s.hydrate);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
hydrate(session);
|
|
16
|
+
}, [hydrate, session]);
|
|
17
|
+
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useCallback } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useSession } from "@/hooks/use-session";
|
|
6
|
+
import { getSession } from "@/lib/auth/actions";
|
|
7
|
+
import { useAuthActions } from "@/store/auth.store";
|
|
8
|
+
|
|
9
|
+
interface ProtectedRouteProps {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* URL to redirect to if not authenticated
|
|
13
|
+
* @default "/login"
|
|
14
|
+
*/
|
|
15
|
+
redirectTo?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Custom loading component
|
|
18
|
+
*/
|
|
19
|
+
loadingComponent?: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Client-side route protection component
|
|
24
|
+
*
|
|
25
|
+
* Wraps content that should only be visible to authenticated users.
|
|
26
|
+
* Redirects to login if user is not authenticated.
|
|
27
|
+
*
|
|
28
|
+
* Note: For server-side protection, use the proxy or check session
|
|
29
|
+
* in server components. This component provides client-side protection
|
|
30
|
+
* as an additional layer.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <ProtectedRoute>
|
|
35
|
+
* <DashboardContent />
|
|
36
|
+
* </ProtectedRoute>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function ProtectedRoute({
|
|
40
|
+
children,
|
|
41
|
+
redirectTo = "/login",
|
|
42
|
+
loadingComponent,
|
|
43
|
+
}: ProtectedRouteProps) {
|
|
44
|
+
const { isAuthenticated, isLoading } = useSession();
|
|
45
|
+
const { handleAuthError } = useAuthActions();
|
|
46
|
+
const router = useRouter();
|
|
47
|
+
|
|
48
|
+
// Revalidate session when window regains focus
|
|
49
|
+
// This detects session expiration when user returns to the tab
|
|
50
|
+
const revalidateSession = useCallback(async () => {
|
|
51
|
+
if (!isAuthenticated) return;
|
|
52
|
+
|
|
53
|
+
const session = await getSession();
|
|
54
|
+
if (!session) {
|
|
55
|
+
// Session expired on backend - update UI state
|
|
56
|
+
handleAuthError();
|
|
57
|
+
}
|
|
58
|
+
}, [isAuthenticated, handleAuthError]);
|
|
59
|
+
|
|
60
|
+
// Focus-based revalidation
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
window.addEventListener("focus", revalidateSession);
|
|
63
|
+
return () => window.removeEventListener("focus", revalidateSession);
|
|
64
|
+
}, [revalidateSession]);
|
|
65
|
+
|
|
66
|
+
// Redirect when not authenticated
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!isLoading && !isAuthenticated) {
|
|
69
|
+
// Include current path as redirect parameter
|
|
70
|
+
const currentPath = window.location.pathname;
|
|
71
|
+
const loginUrl = `${redirectTo}?redirect=${encodeURIComponent(currentPath)}`;
|
|
72
|
+
router.push(loginUrl);
|
|
73
|
+
}
|
|
74
|
+
}, [isAuthenticated, isLoading, redirectTo, router]);
|
|
75
|
+
|
|
76
|
+
// Show loading state
|
|
77
|
+
if (isLoading) {
|
|
78
|
+
return (
|
|
79
|
+
loadingComponent ?? (
|
|
80
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
81
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Don't render content if not authenticated
|
|
88
|
+
if (!isAuthenticated) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return <>{children}</>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* HOC version of ProtectedRoute for class components or convenience
|
|
97
|
+
*/
|
|
98
|
+
export function withProtectedRoute<P extends object>(
|
|
99
|
+
Component: React.ComponentType<P>,
|
|
100
|
+
options?: Omit<ProtectedRouteProps, "children">
|
|
101
|
+
) {
|
|
102
|
+
return function ProtectedComponent(props: P) {
|
|
103
|
+
return (
|
|
104
|
+
<ProtectedRoute {...options}>
|
|
105
|
+
<Component {...props} />
|
|
106
|
+
</ProtectedRoute>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import {
|
|
5
|
+
useDeviceSessionStore,
|
|
6
|
+
useDeviceSessionActions,
|
|
7
|
+
} from "@/store/deviceSession.store";
|
|
8
|
+
import { AUTH_CONFIG } from "@/lib/auth/config";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* DeviceSessionSetup handles:
|
|
12
|
+
* 1. Session initialization on mount
|
|
13
|
+
* 2. Periodic heartbeat (every 5 minutes)
|
|
14
|
+
* 3. Visibility-based activity updates
|
|
15
|
+
*/
|
|
16
|
+
export function DeviceSessionSetup() {
|
|
17
|
+
const deviceSession = useDeviceSessionStore((s) => s.deviceSession);
|
|
18
|
+
const isInitialized = useDeviceSessionStore((s) => s.isInitialized);
|
|
19
|
+
const { initializeSession, sendHeartbeat } = useDeviceSessionActions();
|
|
20
|
+
|
|
21
|
+
// Initialize on mount
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!isInitialized) {
|
|
24
|
+
initializeSession();
|
|
25
|
+
}
|
|
26
|
+
}, [isInitialized, initializeSession]);
|
|
27
|
+
|
|
28
|
+
// Heartbeat - update activity every 5 minutes
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!deviceSession) return;
|
|
31
|
+
|
|
32
|
+
const interval = setInterval(() => {
|
|
33
|
+
sendHeartbeat();
|
|
34
|
+
}, AUTH_CONFIG.deviceHeartbeatInterval);
|
|
35
|
+
|
|
36
|
+
return () => clearInterval(interval);
|
|
37
|
+
}, [deviceSession, sendHeartbeat]);
|
|
38
|
+
|
|
39
|
+
// Update activity on visibility change (tab becomes visible)
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!deviceSession) return;
|
|
42
|
+
|
|
43
|
+
const handleVisibilityChange = () => {
|
|
44
|
+
if (document.visibilityState === "visible") {
|
|
45
|
+
sendHeartbeat();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
50
|
+
return () => {
|
|
51
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
52
|
+
};
|
|
53
|
+
}, [deviceSession, sendHeartbeat]);
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
5
|
+
|
|
6
|
+
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<NextThemesProvider
|
|
9
|
+
attribute="class"
|
|
10
|
+
defaultTheme="system"
|
|
11
|
+
enableSystem
|
|
12
|
+
disableTransitionOnChange
|
|
13
|
+
>
|
|
14
|
+
{children}
|
|
15
|
+
</NextThemesProvider>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Moon, Sun } from "lucide-react";
|
|
5
|
+
import { useTheme } from "next-themes";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
|
|
8
|
+
export function ThemeToggle() {
|
|
9
|
+
const { setTheme, resolvedTheme } = useTheme();
|
|
10
|
+
const [mounted, setMounted] = React.useState(false);
|
|
11
|
+
|
|
12
|
+
// Avoid hydration mismatch by only rendering after mount
|
|
13
|
+
React.useEffect(() => setMounted(true), []);
|
|
14
|
+
|
|
15
|
+
if (!mounted) {
|
|
16
|
+
return (
|
|
17
|
+
<Button variant="ghost" size="icon" aria-label="Toggle theme">
|
|
18
|
+
<span className="h-5 w-5" />
|
|
19
|
+
</Button>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Button
|
|
25
|
+
variant="ghost"
|
|
26
|
+
size="icon"
|
|
27
|
+
onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
|
|
28
|
+
aria-label="Toggle theme"
|
|
29
|
+
>
|
|
30
|
+
<Sun className="h-5 w-5 rotate-0 scale-100 transition-transform dark:-rotate-90 dark:scale-0" />
|
|
31
|
+
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-transform dark:rotate-0 dark:scale-100" />
|
|
32
|
+
</Button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
15
|
+
outline:
|
|
16
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
26
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
27
|
+
icon: "size-9",
|
|
28
|
+
"icon-sm": "size-8",
|
|
29
|
+
"icon-lg": "size-10",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: "default",
|
|
34
|
+
size: "default",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
function Button({
|
|
40
|
+
className,
|
|
41
|
+
variant = "default",
|
|
42
|
+
size = "default",
|
|
43
|
+
asChild = false,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<"button"> &
|
|
46
|
+
VariantProps<typeof buttonVariants> & {
|
|
47
|
+
asChild?: boolean
|
|
48
|
+
}) {
|
|
49
|
+
const Comp = asChild ? Slot : "button"
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
data-slot="button"
|
|
54
|
+
data-variant={variant}
|
|
55
|
+
data-size={size}
|
|
56
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="card"
|
|
9
|
+
className={cn(
|
|
10
|
+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
data-slot="card-header"
|
|
22
|
+
className={cn(
|
|
23
|
+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
data-slot="card-title"
|
|
35
|
+
className={cn("leading-none font-semibold", className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-slot="card-description"
|
|
45
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-slot="card-action"
|
|
55
|
+
className={cn(
|
|
56
|
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
57
|
+
className
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
data-slot="card-content"
|
|
68
|
+
className={cn("px-6", className)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-slot="card-footer"
|
|
78
|
+
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
Card,
|
|
86
|
+
CardHeader,
|
|
87
|
+
CardFooter,
|
|
88
|
+
CardTitle,
|
|
89
|
+
CardAction,
|
|
90
|
+
CardDescription,
|
|
91
|
+
CardContent,
|
|
92
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
6
|
+
return (
|
|
7
|
+
<input
|
|
8
|
+
type={type}
|
|
9
|
+
data-slot="input"
|
|
10
|
+
className={cn(
|
|
11
|
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
12
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
13
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Input }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function Label({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<LabelPrimitive.Root
|
|
14
|
+
data-slot="label"
|
|
15
|
+
className={cn(
|
|
16
|
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { Label }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn("bg-accent animate-pulse rounded-md", className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
interface SpinnerProps {
|
|
4
|
+
size?: "sm" | "md" | "lg";
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Spinner({ size = "md", className }: SpinnerProps) {
|
|
9
|
+
const sizes = { sm: "h-4 w-4 border-2", md: "h-6 w-6 border-2", lg: "h-8 w-8 border-[3px]" };
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
className={cn("animate-spin rounded-full border-muted-foreground border-t-primary", sizes[size], className)}
|
|
14
|
+
role="status"
|
|
15
|
+
aria-label="Loading"
|
|
16
|
+
>
|
|
17
|
+
<span className="sr-only">Loading...</span>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useDeviceSession as useDeviceSessionStore,
|
|
5
|
+
useDeviceSessionActions,
|
|
6
|
+
} from "@/store/deviceSession.store";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook to access the current device session
|
|
10
|
+
*
|
|
11
|
+
* Provides a simplified interface to the device session store.
|
|
12
|
+
* Use this hook in components that need access to the device session.
|
|
13
|
+
*/
|
|
14
|
+
export function useDeviceSession() {
|
|
15
|
+
const { deviceSession, deviceId, isLoading, hasSession, error } = useDeviceSessionStore();
|
|
16
|
+
const { refreshSession, sendHeartbeat, clearError } = useDeviceSessionActions();
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
// Session data
|
|
20
|
+
deviceSession,
|
|
21
|
+
deviceId,
|
|
22
|
+
|
|
23
|
+
// Loading state
|
|
24
|
+
isLoading,
|
|
25
|
+
|
|
26
|
+
// Convenience boolean for session checks
|
|
27
|
+
hasSession,
|
|
28
|
+
|
|
29
|
+
// Error state
|
|
30
|
+
error,
|
|
31
|
+
|
|
32
|
+
// Actions
|
|
33
|
+
refreshSession,
|
|
34
|
+
sendHeartbeat,
|
|
35
|
+
clearError,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Re-export store hooks for direct access when needed
|
|
40
|
+
export { useDeviceSessionStore, useDeviceSessionActions } from "@/store/deviceSession.store";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useAuth, useAuthActions } from "@/store/auth.store";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to access the current session
|
|
7
|
+
*
|
|
8
|
+
* Provides a simplified interface to the auth store.
|
|
9
|
+
* Use this hook in components that need access to the current user/session.
|
|
10
|
+
*/
|
|
11
|
+
export function useSession() {
|
|
12
|
+
const { session, user, isLoading, isAuthenticated, error } = useAuth();
|
|
13
|
+
const { signOut, clearError } = useAuthActions();
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
// Session data
|
|
17
|
+
session,
|
|
18
|
+
user,
|
|
19
|
+
|
|
20
|
+
// Loading state
|
|
21
|
+
isLoading,
|
|
22
|
+
|
|
23
|
+
// Convenience boolean for auth checks
|
|
24
|
+
isAuthenticated,
|
|
25
|
+
|
|
26
|
+
// Error state
|
|
27
|
+
error,
|
|
28
|
+
|
|
29
|
+
// Actions
|
|
30
|
+
signOut,
|
|
31
|
+
clearError,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hook to require authentication
|
|
37
|
+
*
|
|
38
|
+
* Returns the session and throws if not authenticated.
|
|
39
|
+
* Use this in components that should only render when authenticated.
|
|
40
|
+
*/
|
|
41
|
+
export function useRequireSession() {
|
|
42
|
+
const { session, user, isLoading, isAuthenticated } = useAuth();
|
|
43
|
+
|
|
44
|
+
if (!isLoading && !isAuthenticated) {
|
|
45
|
+
throw new Error("Authentication required");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
session: session!,
|
|
50
|
+
user: user!,
|
|
51
|
+
isLoading,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Re-export store hooks for direct access when needed
|
|
56
|
+
export { useAuth, useAuthActions } from "@/store/auth.store";
|