@getjack/jack 0.1.32 → 0.1.33
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/package.json +1 -1
- package/src/commands/deploys.ts +95 -0
- package/src/commands/link.ts +8 -0
- package/src/commands/mcp.ts +179 -4
- package/src/commands/rollback.ts +53 -0
- package/src/commands/services.ts +11 -1
- package/src/commands/ship.ts +3 -1
- package/src/commands/tokens.ts +16 -1
- package/src/commands/whoami.ts +43 -8
- package/src/index.ts +16 -0
- package/src/lib/agent-files.ts +54 -4
- package/src/lib/agent-integration.ts +4 -166
- package/src/lib/claude-hooks-installer.ts +55 -0
- package/src/lib/control-plane.ts +78 -40
- package/src/lib/debug.ts +2 -1
- package/src/lib/deploy-upload.ts +6 -0
- package/src/lib/hooks.ts +3 -1
- package/src/lib/managed-deploy.ts +12 -9
- package/src/lib/project-link.ts +6 -0
- package/src/lib/project-operations.ts +68 -22
- package/src/lib/telemetry.ts +2 -0
- package/src/mcp/README.md +1 -1
- package/src/mcp/resources/index.ts +1 -16
- package/src/mcp/server.ts +23 -0
- package/src/mcp/tools/index.ts +133 -17
- package/src/mcp/types.ts +1 -0
- package/src/mcp/utils.ts +2 -1
- package/src/templates/index.ts +25 -73
- package/templates/CLAUDE.md +41 -0
- package/templates/ai-chat/.jack.json +10 -5
- package/templates/ai-chat/bun.lock +50 -1
- package/templates/ai-chat/package.json +5 -0
- package/templates/ai-chat/public/app.js +73 -0
- package/templates/ai-chat/public/index.html +14 -197
- package/templates/ai-chat/schema.sql +14 -0
- package/templates/ai-chat/src/index.ts +86 -102
- package/templates/ai-chat/wrangler.jsonc +8 -1
- package/templates/cron/.jack.json +66 -0
- package/templates/cron/bun.lock +23 -0
- package/templates/cron/package.json +16 -0
- package/templates/cron/schema.sql +24 -0
- package/templates/cron/src/index.ts +117 -0
- package/templates/cron/src/jobs.ts +139 -0
- package/templates/cron/src/webhooks.ts +95 -0
- package/templates/cron/tsconfig.json +17 -0
- package/templates/cron/wrangler.jsonc +11 -0
- package/templates/miniapp/.jack.json +1 -1
- package/templates/nextjs/.jack.json +1 -1
- package/templates/nextjs-auth/.jack.json +44 -0
- package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
- package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
- package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
- package/templates/nextjs-auth/app/error.tsx +44 -0
- package/templates/nextjs-auth/app/globals.css +1 -0
- package/templates/nextjs-auth/app/health/route.ts +3 -0
- package/templates/nextjs-auth/app/layout.tsx +24 -0
- package/templates/nextjs-auth/app/login/page.tsx +10 -0
- package/templates/nextjs-auth/app/page.tsx +86 -0
- package/templates/nextjs-auth/app/signup/page.tsx +10 -0
- package/templates/nextjs-auth/bun.lock +1065 -0
- package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
- package/templates/nextjs-auth/components/auth-form.tsx +191 -0
- package/templates/nextjs-auth/components/header.tsx +50 -0
- package/templates/nextjs-auth/components/user-menu.tsx +23 -0
- package/templates/nextjs-auth/lib/auth-client.ts +3 -0
- package/templates/nextjs-auth/lib/auth.ts +43 -0
- package/templates/nextjs-auth/lib/utils.ts +6 -0
- package/templates/nextjs-auth/middleware.ts +33 -0
- package/templates/nextjs-auth/next.config.ts +8 -0
- package/templates/nextjs-auth/open-next.config.ts +6 -0
- package/templates/nextjs-auth/package.json +33 -0
- package/templates/nextjs-auth/postcss.config.mjs +8 -0
- package/templates/nextjs-auth/schema.sql +49 -0
- package/templates/nextjs-auth/tsconfig.json +28 -0
- package/templates/nextjs-auth/wrangler.jsonc +23 -0
- package/templates/nextjs-clerk/.jack.json +54 -0
- package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
- package/templates/nextjs-clerk/app/globals.css +1 -0
- package/templates/nextjs-clerk/app/health/route.ts +3 -0
- package/templates/nextjs-clerk/app/layout.tsx +26 -0
- package/templates/nextjs-clerk/app/page.tsx +86 -0
- package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
- package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
- package/templates/nextjs-clerk/bun.lock +1055 -0
- package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
- package/templates/nextjs-clerk/components/header.tsx +40 -0
- package/templates/nextjs-clerk/lib/utils.ts +6 -0
- package/templates/nextjs-clerk/middleware.ts +18 -0
- package/templates/nextjs-clerk/next.config.ts +8 -0
- package/templates/nextjs-clerk/open-next.config.ts +6 -0
- package/templates/nextjs-clerk/package.json +31 -0
- package/templates/nextjs-clerk/postcss.config.mjs +8 -0
- package/templates/nextjs-clerk/tsconfig.json +28 -0
- package/templates/nextjs-clerk/wrangler.jsonc +17 -0
- package/templates/nextjs-shadcn/.jack.json +34 -0
- package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
- package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
- package/templates/nextjs-shadcn/app/globals.css +126 -0
- package/templates/nextjs-shadcn/app/health/route.ts +3 -0
- package/templates/nextjs-shadcn/app/layout.tsx +24 -0
- package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
- package/templates/nextjs-shadcn/app/page.tsx +180 -0
- package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
- package/templates/nextjs-shadcn/bun.lock +1789 -0
- package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
- package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
- package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
- package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
- package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
- package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
- package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
- package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
- package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
- package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
- package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
- package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
- package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
- package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
- package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
- package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
- package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
- package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
- package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
- package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
- package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
- package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
- package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
- package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
- package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
- package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
- package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
- package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
- package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
- package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
- package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
- package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
- package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
- package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
- package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
- package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
- package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
- package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
- package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
- package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
- package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
- package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
- package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
- package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
- package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
- package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
- package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
- package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
- package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
- package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
- package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
- package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
- package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
- package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
- package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
- package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
- package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
- package/templates/nextjs-shadcn/components.json +23 -0
- package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
- package/templates/nextjs-shadcn/lib/utils.ts +6 -0
- package/templates/nextjs-shadcn/next-env.d.ts +6 -0
- package/templates/nextjs-shadcn/next.config.ts +8 -0
- package/templates/nextjs-shadcn/open-next.config.ts +6 -0
- package/templates/nextjs-shadcn/package.json +55 -0
- package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
- package/templates/nextjs-shadcn/tsconfig.json +28 -0
- package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
- package/templates/resend/.jack.json +64 -0
- package/templates/resend/bun.lock +23 -0
- package/templates/resend/package.json +16 -0
- package/templates/resend/schema.sql +13 -0
- package/templates/resend/src/email.ts +165 -0
- package/templates/resend/src/index.ts +108 -0
- package/templates/resend/tsconfig.json +17 -0
- package/templates/resend/wrangler.jsonc +11 -0
- package/templates/saas/.jack.json +1 -1
- package/templates/ai-chat/public/chat.js +0 -149
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { authClient } from "@/lib/auth-client";
|
|
4
|
+
import { Check, Eye, EyeOff, Loader2, Mail } from "lucide-react";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { useSearchParams } from "next/navigation";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
|
|
9
|
+
interface AuthFormProps {
|
|
10
|
+
mode: "login" | "signup";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AuthForm({ mode }: AuthFormProps) {
|
|
14
|
+
const searchParams = useSearchParams();
|
|
15
|
+
const [email, setEmail] = useState("");
|
|
16
|
+
const [password, setPassword] = useState("");
|
|
17
|
+
const [name, setName] = useState("");
|
|
18
|
+
const [error, setError] = useState("");
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
21
|
+
const [success, setSuccess] = useState(false);
|
|
22
|
+
|
|
23
|
+
const isLogin = mode === "login";
|
|
24
|
+
const rawCallback = isLogin ? searchParams.get("callbackUrl") : null;
|
|
25
|
+
const callbackUrl =
|
|
26
|
+
rawCallback?.startsWith("/") && !rawCallback.startsWith("//") ? rawCallback : "/dashboard";
|
|
27
|
+
const title = isLogin ? "Sign in to your account" : "Create your account";
|
|
28
|
+
const submitLabel = isLogin ? "Sign In" : "Sign Up";
|
|
29
|
+
const switchText = isLogin ? "Don't have an account?" : "Already have an account?";
|
|
30
|
+
const switchHref = isLogin ? "/signup" : "/login";
|
|
31
|
+
const switchLabel = isLogin ? "Sign up" : "Sign in";
|
|
32
|
+
|
|
33
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
setError("");
|
|
36
|
+
setLoading(true);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (isLogin) {
|
|
40
|
+
const result = await authClient.signIn.email({
|
|
41
|
+
email,
|
|
42
|
+
password,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (result.error) {
|
|
46
|
+
setError(result.error.message || "Invalid email or password");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Full reload ensures middleware + server components re-evaluate with new auth state
|
|
51
|
+
window.location.href = callbackUrl;
|
|
52
|
+
} else {
|
|
53
|
+
const result = await authClient.signUp.email({
|
|
54
|
+
email,
|
|
55
|
+
password,
|
|
56
|
+
name: name || email.split("@")[0],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.error) {
|
|
60
|
+
setError(result.error.message || "Failed to create account");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setSuccess(true);
|
|
65
|
+
setTimeout(() => {
|
|
66
|
+
window.location.href = "/dashboard";
|
|
67
|
+
}, 800);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
setError("Something went wrong. Please try again.");
|
|
72
|
+
} finally {
|
|
73
|
+
setLoading(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (success) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
80
|
+
<div className="w-full max-w-sm text-center">
|
|
81
|
+
<div className="mx-auto mb-4 flex size-12 items-center justify-center rounded-full bg-green-100">
|
|
82
|
+
<Check className="size-6 text-green-600" />
|
|
83
|
+
</div>
|
|
84
|
+
<h1 className="text-xl font-bold">Account created!</h1>
|
|
85
|
+
<p className="mt-2 text-sm text-gray-500">Redirecting to your dashboard...</p>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex min-h-screen items-center justify-center px-4">
|
|
93
|
+
<div className="w-full max-w-sm">
|
|
94
|
+
<div className="mb-8 text-center">
|
|
95
|
+
<Link
|
|
96
|
+
href="/"
|
|
97
|
+
className="mb-6 inline-flex items-center gap-2 text-sm text-gray-500 transition hover:text-gray-700"
|
|
98
|
+
>
|
|
99
|
+
← Back
|
|
100
|
+
</Link>
|
|
101
|
+
<h1 className="text-2xl font-bold">{title}</h1>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
105
|
+
{!isLogin && (
|
|
106
|
+
<div>
|
|
107
|
+
<label htmlFor="name" className="mb-1.5 block text-sm font-medium text-gray-700">
|
|
108
|
+
Name
|
|
109
|
+
</label>
|
|
110
|
+
<input
|
|
111
|
+
id="name"
|
|
112
|
+
type="text"
|
|
113
|
+
value={name}
|
|
114
|
+
onChange={(e) => setName(e.target.value)}
|
|
115
|
+
placeholder="Your name"
|
|
116
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-500 focus:ring-2 focus:ring-gray-200"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
<div>
|
|
122
|
+
<label htmlFor="email" className="mb-1.5 block text-sm font-medium text-gray-700">
|
|
123
|
+
Email
|
|
124
|
+
</label>
|
|
125
|
+
<input
|
|
126
|
+
id="email"
|
|
127
|
+
type="email"
|
|
128
|
+
value={email}
|
|
129
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
130
|
+
placeholder="you@example.com"
|
|
131
|
+
required
|
|
132
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-500 focus:ring-2 focus:ring-gray-200"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div>
|
|
137
|
+
<label htmlFor="password" className="mb-1.5 block text-sm font-medium text-gray-700">
|
|
138
|
+
Password
|
|
139
|
+
</label>
|
|
140
|
+
<div className="relative">
|
|
141
|
+
<input
|
|
142
|
+
id="password"
|
|
143
|
+
type={showPassword ? "text" : "password"}
|
|
144
|
+
value={password}
|
|
145
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
146
|
+
placeholder="Enter your password"
|
|
147
|
+
required
|
|
148
|
+
minLength={8}
|
|
149
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 pr-9 text-sm outline-none transition focus:border-gray-500 focus:ring-2 focus:ring-gray-200"
|
|
150
|
+
/>
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={() => setShowPassword(!showPassword)}
|
|
154
|
+
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
155
|
+
tabIndex={-1}
|
|
156
|
+
>
|
|
157
|
+
{showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{error && (
|
|
163
|
+
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
|
164
|
+
{error}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
<button
|
|
169
|
+
type="submit"
|
|
170
|
+
disabled={loading}
|
|
171
|
+
className="flex w-full items-center justify-center gap-2 rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50"
|
|
172
|
+
>
|
|
173
|
+
{loading ? <Loader2 className="size-4 animate-spin" /> : <Mail className="size-4" />}
|
|
174
|
+
{submitLabel}
|
|
175
|
+
</button>
|
|
176
|
+
</form>
|
|
177
|
+
|
|
178
|
+
{/* Social login (GitHub, Google) is pre-wired in lib/auth.ts.
|
|
179
|
+
To enable: add GITHUB_CLIENT_ID + GITHUB_CLIENT_SECRET or
|
|
180
|
+
GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET via `jack secrets` */}
|
|
181
|
+
|
|
182
|
+
<p className="mt-6 text-center text-sm text-gray-500">
|
|
183
|
+
{switchText}{" "}
|
|
184
|
+
<Link href={switchHref} className="font-medium text-gray-900 hover:underline">
|
|
185
|
+
{switchLabel}
|
|
186
|
+
</Link>
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Shield } from "lucide-react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
interface HeaderProps {
|
|
5
|
+
user?: {
|
|
6
|
+
name: string | null;
|
|
7
|
+
email: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Header({ user }: HeaderProps) {
|
|
12
|
+
return (
|
|
13
|
+
<header className="border-b border-gray-200 bg-white">
|
|
14
|
+
<div className="mx-auto flex h-14 max-w-3xl items-center justify-between px-6">
|
|
15
|
+
<Link href="/" className="flex items-center gap-2 font-semibold">
|
|
16
|
+
<div className="flex size-7 items-center justify-center rounded-md bg-gray-900 text-white text-xs">
|
|
17
|
+
<Shield className="size-4" />
|
|
18
|
+
</div>
|
|
19
|
+
jack-template
|
|
20
|
+
</Link>
|
|
21
|
+
|
|
22
|
+
<nav className="flex items-center gap-1">
|
|
23
|
+
{user ? (
|
|
24
|
+
<Link
|
|
25
|
+
href="/dashboard"
|
|
26
|
+
className="rounded-md px-3 py-1.5 text-sm font-medium text-gray-600 transition hover:bg-gray-100 hover:text-gray-900"
|
|
27
|
+
>
|
|
28
|
+
Dashboard
|
|
29
|
+
</Link>
|
|
30
|
+
) : (
|
|
31
|
+
<>
|
|
32
|
+
<Link
|
|
33
|
+
href="/login"
|
|
34
|
+
className="rounded-md px-3 py-1.5 text-sm font-medium text-gray-600 transition hover:bg-gray-100 hover:text-gray-900"
|
|
35
|
+
>
|
|
36
|
+
Sign In
|
|
37
|
+
</Link>
|
|
38
|
+
<Link
|
|
39
|
+
href="/signup"
|
|
40
|
+
className="rounded-md bg-gray-900 px-3 py-1.5 text-sm font-medium text-white transition hover:bg-gray-800"
|
|
41
|
+
>
|
|
42
|
+
Sign Up
|
|
43
|
+
</Link>
|
|
44
|
+
</>
|
|
45
|
+
)}
|
|
46
|
+
</nav>
|
|
47
|
+
</div>
|
|
48
|
+
</header>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { authClient } from "@/lib/auth-client";
|
|
4
|
+
import { LogOut } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export function UserMenu() {
|
|
7
|
+
async function handleSignOut() {
|
|
8
|
+
await authClient.signOut();
|
|
9
|
+
// Full reload after auth state change ensures middleware + server components re-evaluate
|
|
10
|
+
window.location.href = "/";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
onClick={handleSignOut}
|
|
17
|
+
className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50"
|
|
18
|
+
>
|
|
19
|
+
<LogOut className="size-4" />
|
|
20
|
+
Sign Out
|
|
21
|
+
</button>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getCloudflareContext } from "@opennextjs/cloudflare";
|
|
2
|
+
import { betterAuth } from "better-auth";
|
|
3
|
+
import { Kysely } from "kysely";
|
|
4
|
+
import { D1Dialect } from "kysely-d1";
|
|
5
|
+
|
|
6
|
+
export function createAuth(d1: D1Database, env: Record<string, string | undefined>) {
|
|
7
|
+
// biome-ignore lint/suspicious/noExplicitAny: D1 has no typed schema
|
|
8
|
+
const db = new Kysely<any>({
|
|
9
|
+
dialect: new D1Dialect({ database: d1 }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return betterAuth({
|
|
13
|
+
database: {
|
|
14
|
+
db,
|
|
15
|
+
type: "sqlite",
|
|
16
|
+
},
|
|
17
|
+
secret: env.BETTER_AUTH_SECRET,
|
|
18
|
+
emailAndPassword: { enabled: true },
|
|
19
|
+
socialProviders: {
|
|
20
|
+
...(env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET
|
|
21
|
+
? {
|
|
22
|
+
github: {
|
|
23
|
+
clientId: env.GITHUB_CLIENT_ID,
|
|
24
|
+
clientSecret: env.GITHUB_CLIENT_SECRET,
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
: {}),
|
|
28
|
+
...(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET
|
|
29
|
+
? {
|
|
30
|
+
google: {
|
|
31
|
+
clientId: env.GOOGLE_CLIENT_ID,
|
|
32
|
+
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
: {}),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function getAuth() {
|
|
41
|
+
const { env } = await getCloudflareContext();
|
|
42
|
+
return createAuth(env.DB, env as unknown as Record<string, string | undefined>);
|
|
43
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
const protectedPaths = ["/dashboard"];
|
|
4
|
+
|
|
5
|
+
export function middleware(request: NextRequest) {
|
|
6
|
+
const { pathname } = request.nextUrl;
|
|
7
|
+
|
|
8
|
+
const isProtected = protectedPaths.some(
|
|
9
|
+
(path) => pathname === path || pathname.startsWith(`${path}/`),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
if (!isProtected) {
|
|
13
|
+
return NextResponse.next();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Edge-safe session check: only inspect the cookie, do not call auth.api.getSession()
|
|
17
|
+
// because that requires Node.js APIs (perf_hooks) unavailable in edge middleware.
|
|
18
|
+
const sessionCookie =
|
|
19
|
+
request.cookies.get("better-auth.session_token") ??
|
|
20
|
+
request.cookies.get("__Secure-better-auth.session_token");
|
|
21
|
+
|
|
22
|
+
if (!sessionCookie?.value) {
|
|
23
|
+
const loginUrl = new URL("/login", request.url);
|
|
24
|
+
loginUrl.searchParams.set("callbackUrl", pathname);
|
|
25
|
+
return NextResponse.redirect(loginUrl);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return NextResponse.next();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const config = {
|
|
32
|
+
matcher: ["/dashboard/:path*"],
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jack-template",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
|
9
|
+
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
|
10
|
+
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@opennextjs/cloudflare": "^1.0.0",
|
|
14
|
+
"@tailwindcss/postcss": "^4.1.18",
|
|
15
|
+
"better-auth": "^1",
|
|
16
|
+
"clsx": "^2.1.1",
|
|
17
|
+
"kysely": "^0.27",
|
|
18
|
+
"kysely-d1": "^0.3",
|
|
19
|
+
"lucide-react": "^0.563.0",
|
|
20
|
+
"next": "^15.0.0",
|
|
21
|
+
"postcss": "^8.5.6",
|
|
22
|
+
"react": "^19.0.0",
|
|
23
|
+
"react-dom": "^19.0.0",
|
|
24
|
+
"tailwind-merge": "^3.4.0",
|
|
25
|
+
"tailwindcss": "^4.1.18"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
29
|
+
"@types/react": "^19.0.0",
|
|
30
|
+
"@types/react-dom": "^19.0.0",
|
|
31
|
+
"typescript": "^5.6.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS user (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
name TEXT NOT NULL,
|
|
4
|
+
email TEXT NOT NULL UNIQUE,
|
|
5
|
+
emailVerified INTEGER NOT NULL DEFAULT 0,
|
|
6
|
+
image TEXT,
|
|
7
|
+
createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
8
|
+
updatedAt INTEGER NOT NULL DEFAULT (unixepoch())
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS session (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
expiresAt INTEGER NOT NULL,
|
|
14
|
+
token TEXT NOT NULL UNIQUE,
|
|
15
|
+
ipAddress TEXT,
|
|
16
|
+
userAgent TEXT,
|
|
17
|
+
userId TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
|
|
18
|
+
createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
19
|
+
updatedAt INTEGER NOT NULL DEFAULT (unixepoch())
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS account (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
accountId TEXT NOT NULL,
|
|
25
|
+
providerId TEXT NOT NULL,
|
|
26
|
+
userId TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
|
|
27
|
+
accessToken TEXT,
|
|
28
|
+
refreshToken TEXT,
|
|
29
|
+
idToken TEXT,
|
|
30
|
+
accessTokenExpiresAt INTEGER,
|
|
31
|
+
refreshTokenExpiresAt INTEGER,
|
|
32
|
+
scope TEXT,
|
|
33
|
+
password TEXT,
|
|
34
|
+
createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
35
|
+
updatedAt INTEGER NOT NULL DEFAULT (unixepoch())
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS verification (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
identifier TEXT NOT NULL,
|
|
41
|
+
value TEXT NOT NULL,
|
|
42
|
+
expiresAt INTEGER NOT NULL,
|
|
43
|
+
createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
44
|
+
updatedAt INTEGER NOT NULL DEFAULT (unixepoch())
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_session_token ON session(token);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_session_userId ON session(userId);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_account_userId ON account(userId);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "preserve",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"incremental": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/*": ["./*"]
|
|
17
|
+
},
|
|
18
|
+
"plugins": [
|
|
19
|
+
{
|
|
20
|
+
"name": "next"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"types": ["@cloudflare/workers-types"],
|
|
24
|
+
"allowJs": true
|
|
25
|
+
},
|
|
26
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "cloudflare-env.d.ts"],
|
|
27
|
+
"exclude": ["node_modules"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "jack-template",
|
|
4
|
+
"main": ".open-next/worker.js",
|
|
5
|
+
"compatibility_date": "2024-12-30",
|
|
6
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
7
|
+
"assets": {
|
|
8
|
+
"directory": ".open-next/assets",
|
|
9
|
+
"binding": "ASSETS"
|
|
10
|
+
},
|
|
11
|
+
"d1_databases": [
|
|
12
|
+
{
|
|
13
|
+
"binding": "DB",
|
|
14
|
+
"database_name": "jack-template-db"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"r2_buckets": [
|
|
18
|
+
{
|
|
19
|
+
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
|
20
|
+
"bucket_name": "jack-template-cache"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextjs-clerk",
|
|
3
|
+
"description": "Next.js + Clerk (managed auth with pre-built UI components)",
|
|
4
|
+
"secrets": ["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY"],
|
|
5
|
+
"capabilities": [],
|
|
6
|
+
"intent": {
|
|
7
|
+
"keywords": ["clerk", "auth", "login", "nextjs", "managed-auth", "sso"],
|
|
8
|
+
"examples": ["app with managed auth", "clerk login", "quick auth setup"]
|
|
9
|
+
},
|
|
10
|
+
"agentContext": {
|
|
11
|
+
"summary": "A Next.js app with Clerk managed authentication, pre-built sign-in/sign-up components, and route protection middleware.",
|
|
12
|
+
"full_text": "## Project Structure\n\n- `app/layout.tsx` - Root layout with ClerkProvider\n- `app/page.tsx` - Landing page with sign-in CTA\n- `app/sign-in/[[...sign-in]]/page.tsx` - Clerk sign-in page\n- `app/sign-up/[[...sign-up]]/page.tsx` - Clerk sign-up page\n- `app/dashboard/page.tsx` - Protected dashboard page\n- `middleware.ts` - Clerk auth middleware for route protection\n- `components/header.tsx` - Header with UserButton for profile/logout\n\n## Authentication\n\nUses Clerk for fully managed auth. No database tables needed for auth.\n\n### Route Protection\n\nThe `middleware.ts` uses `clerkMiddleware()` with `createRouteMatcher()` to protect routes. By default, `/dashboard` and `/dashboard/*` are protected.\n\n### Client Components\n\n```tsx\nimport { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs';\n\n// Show sign-in button when signed out\n<SignedOut><SignInButton /></SignedOut>\n\n// Show user button when signed in\n<SignedIn><UserButton /></SignedIn>\n```\n\n### Server Components\n\n```tsx\nimport { auth, currentUser } from '@clerk/nextjs/server';\n\n// Get auth state\nconst { userId } = await auth();\n\n// Get full user object\nconst user = await currentUser();\n```\n\n## Environment Variables\n\n- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` - Clerk publishable key (pk_test_...)\n- `CLERK_SECRET_KEY` - Clerk secret key (sk_test_...)\n\nGet these from https://dashboard.clerk.com\n\n## OpenNext on Cloudflare — Important Rules\n\nThis app runs via OpenNext on Cloudflare Workers. Follow these rules:\n\n### Use window.location.href instead of router.push() for full-page transitions\nOpenNext has a broken webpack chunk URL resolver. `router.push()` to pages whose chunks aren't loaded fails with `ChunkLoadError`. Use `window.location.href` for auth state changes or cross-section navigation. `<Link>` components work fine.\n\n### Add `export const dynamic = 'force-dynamic'` to pages using getCloudflareContext()\nWithout this, static prerendering fails because `getCloudflareContext()` is only available at request time.\n\n### Edge middleware cannot use Node.js APIs\nDo not import Node.js built-in modules in middleware. Clerk's `clerkMiddleware()` is edge-compatible.\n\n## Resources\n\n- [Clerk Docs](https://clerk.com/docs)\n- [Clerk Next.js Quickstart](https://clerk.com/docs/quickstarts/nextjs)\n- [OpenNext Docs](https://opennext.js.org/cloudflare)"
|
|
13
|
+
},
|
|
14
|
+
"hooks": {
|
|
15
|
+
"preCreate": [
|
|
16
|
+
{
|
|
17
|
+
"action": "require",
|
|
18
|
+
"source": "secret",
|
|
19
|
+
"key": "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
|
|
20
|
+
"message": "Clerk publishable key",
|
|
21
|
+
"setupUrl": "https://dashboard.clerk.com",
|
|
22
|
+
"onMissing": "prompt",
|
|
23
|
+
"promptMessage": "Enter Clerk Publishable Key (pk_test_...):"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"action": "require",
|
|
27
|
+
"source": "secret",
|
|
28
|
+
"key": "CLERK_SECRET_KEY",
|
|
29
|
+
"message": "Clerk secret key",
|
|
30
|
+
"setupUrl": "https://dashboard.clerk.com",
|
|
31
|
+
"onMissing": "prompt",
|
|
32
|
+
"promptMessage": "Enter Clerk Secret Key (sk_test_...):"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"postDeploy": [
|
|
36
|
+
{
|
|
37
|
+
"action": "clipboard",
|
|
38
|
+
"text": "{{url}}",
|
|
39
|
+
"message": "Deploy URL copied to clipboard"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"action": "box",
|
|
43
|
+
"title": "Auth ready: {{name}}",
|
|
44
|
+
"lines": [
|
|
45
|
+
"{{url}}",
|
|
46
|
+
"",
|
|
47
|
+
"Clerk dashboard: https://dashboard.clerk.com",
|
|
48
|
+
"Sign-up and login pages work immediately.",
|
|
49
|
+
"Configure social providers in Clerk dashboard."
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { currentUser } from "@clerk/nextjs/server";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
|
|
4
|
+
export default async function DashboardPage() {
|
|
5
|
+
const user = await currentUser();
|
|
6
|
+
|
|
7
|
+
if (!user) {
|
|
8
|
+
redirect("/sign-in");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<main className="mx-auto max-w-3xl px-6 py-12">
|
|
13
|
+
<h1 className="text-2xl font-bold">Dashboard</h1>
|
|
14
|
+
<p className="mt-1 text-gray-500">Welcome back. This page is protected by Clerk middleware.</p>
|
|
15
|
+
|
|
16
|
+
<div className="mt-8 rounded-xl border border-gray-200 bg-white p-6">
|
|
17
|
+
<h2 className="text-lg font-semibold">Your Profile</h2>
|
|
18
|
+
<dl className="mt-4 space-y-3">
|
|
19
|
+
<div className="flex gap-2">
|
|
20
|
+
<dt className="w-28 shrink-0 text-sm font-medium text-gray-500">Name</dt>
|
|
21
|
+
<dd className="text-sm">
|
|
22
|
+
{user.firstName} {user.lastName}
|
|
23
|
+
</dd>
|
|
24
|
+
</div>
|
|
25
|
+
<div className="flex gap-2">
|
|
26
|
+
<dt className="w-28 shrink-0 text-sm font-medium text-gray-500">Email</dt>
|
|
27
|
+
<dd className="text-sm">{user.emailAddresses[0]?.emailAddress}</dd>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="flex gap-2">
|
|
30
|
+
<dt className="w-28 shrink-0 text-sm font-medium text-gray-500">User ID</dt>
|
|
31
|
+
<dd className="text-sm font-mono text-gray-400">{user.id}</dd>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="flex gap-2">
|
|
34
|
+
<dt className="w-28 shrink-0 text-sm font-medium text-gray-500">Joined</dt>
|
|
35
|
+
<dd className="text-sm">
|
|
36
|
+
{user.createdAt ? new Date(user.createdAt).toLocaleDateString() : "Unknown"}
|
|
37
|
+
</dd>
|
|
38
|
+
</div>
|
|
39
|
+
</dl>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div className="mt-6 rounded-xl border border-gray-200 bg-white p-6">
|
|
43
|
+
<h2 className="text-lg font-semibold">Next Steps</h2>
|
|
44
|
+
<ul className="mt-3 space-y-2 text-sm text-gray-600">
|
|
45
|
+
<li>
|
|
46
|
+
Add social providers in the{" "}
|
|
47
|
+
<a
|
|
48
|
+
href="https://dashboard.clerk.com"
|
|
49
|
+
target="_blank"
|
|
50
|
+
rel="noopener noreferrer"
|
|
51
|
+
className="font-medium text-gray-900 underline underline-offset-2"
|
|
52
|
+
>
|
|
53
|
+
Clerk dashboard
|
|
54
|
+
</a>
|
|
55
|
+
</li>
|
|
56
|
+
<li>Protect more routes by editing <code className="rounded bg-gray-100 px-1 py-0.5 text-xs">middleware.ts</code></li>
|
|
57
|
+
<li>
|
|
58
|
+
Access user data in server components with{" "}
|
|
59
|
+
<code className="rounded bg-gray-100 px-1 py-0.5 text-xs">currentUser()</code>
|
|
60
|
+
</li>
|
|
61
|
+
<li>
|
|
62
|
+
Access auth state in client components with{" "}
|
|
63
|
+
<code className="rounded bg-gray-100 px-1 py-0.5 text-xs">useUser()</code>
|
|
64
|
+
</li>
|
|
65
|
+
</ul>
|
|
66
|
+
</div>
|
|
67
|
+
</main>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|