Skip to content

Commit 44b86ba

Browse files
committed
feat: restructure authed routes to dashboard and add setup auth
1 parent 9762cbd commit 44b86ba

29 files changed

+798
-116
lines changed

.dev.vars.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ CLOUDFLARE_API_TOKEN=your-api-token-here
1111

1212
# Authentication
1313
BETTER_AUTH_SECRET=your-better-auth-secret # see https://www.better-auth.com/docs/installation
14-
BETTER_AUTH_URL=http://localhost:3000 # Base URL of your app
14+
BETTER_AUTH_URL=http://localhost:3000 # Base URL of your app - should use the deployed URL in Workers for prod
1515

1616
# Google Auth : see https://www.better-auth.com/docs/authentication/google
1717
GOOGLE_CLIENT_ID=your-google-client-id

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ CLOUDFLARE_ACCOUNT_ID=your-account-id-here
8888
CLOUDFLARE_D1_TOKEN=your-api-token-here
8989
```
9090
91+
### Add Environment Secret to Workers
92+
93+
To store all the secrets on Workers for production, you can refer to the following documentations https://developers.cloudflare.com/workers/configuration/secrets/. Basically, all the secrets such as `GOOGLE_CLIENT_ID` and others should be kept on Cloudflare Secrets.
94+
9195
## 3. Database Setup
9296
9397
### Option A: Use existing database (Recommended)

cloudflare-env.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
/* eslint-disable */
2-
// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts` (hash: 44497be1ce2e308eac95318919d21123)
2+
// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts` (hash: 27c9b47fcb70f49031934291d4d721fa)
33
// Runtime types generated with workerd@1.20250906.0 2025-03-01 global_fetch_strictly_public,nodejs_compat
44
declare namespace Cloudflare {
55
interface Env {
66
NEXTJS_ENV: string;
77
CLOUDFLARE_ACCOUNT_ID: string;
88
CLOUDFLARE_D1_TOKEN: string;
99
CLOUDFLARE_R2_URL: string;
10+
CLOUDFLARE_API_TOKEN: string;
11+
BETTER_AUTH_SECRET: string;
12+
BETTER_AUTH_URL: string;
13+
GOOGLE_CLIENT_ID: string;
14+
GOOGLE_CLIENT_SECRET: string;
1015
next_cf_app_bucket: R2Bucket;
1116
next_cf_app: D1Database;
1217
ASSETS: Fetcher;

middleware.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { type NextRequest, NextResponse } from "next/server";
2+
import { auth } from "./src/lib/auth";
3+
4+
export async function middleware(request: NextRequest) {
5+
try {
6+
// Validate session, not just checking for cookie
7+
const session = await auth.api.getSession({
8+
headers: request.headers,
9+
});
10+
11+
if (!session) {
12+
return NextResponse.redirect(new URL("/login", request.url));
13+
}
14+
15+
return NextResponse.next();
16+
} catch (_error) {
17+
// If session validation fails, redirect to login
18+
return NextResponse.redirect(new URL("/login", request.url));
19+
}
20+
}
21+
22+
export const config = {
23+
matcher: [
24+
"/dashboard/:path*", // Protects /dashboard and all sub-routes
25+
],
26+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"react": "19.1.0",
4747
"react-dom": "19.1.0",
4848
"react-hook-form": "^7.62.0",
49+
"react-hot-toast": "^2.6.0",
4950
"tailwind-merge": "^3.3.1",
5051
"zod": "^4.1.8"
5152
},

pnpm-lock.yaml

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/actions/auth.action.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"use server";
2+
3+
import { auth } from "@/lib/auth";
4+
import {
5+
type AuthResponse,
6+
type SignInSchema,
7+
type SignUpSchema,
8+
signInSchema,
9+
signUpSchema,
10+
} from "@/lib/validations/auth.validation";
11+
12+
// Re-export schemas for convenience
13+
export { signInSchema, signUpSchema };
14+
15+
// #region SERVER ACTIONS
16+
17+
export const signIn = async ({
18+
email,
19+
password,
20+
}: SignInSchema): Promise<AuthResponse> => {
21+
try {
22+
await auth.api.signInEmail({
23+
body: {
24+
email,
25+
password,
26+
},
27+
});
28+
29+
return {
30+
success: true,
31+
message: "Signed in succesfully",
32+
};
33+
} catch (error) {
34+
const err = error as Error;
35+
return {
36+
success: false,
37+
message: err.message || "An unknown error occured.",
38+
};
39+
}
40+
};
41+
42+
export const signUp = async ({
43+
email,
44+
password,
45+
username,
46+
}: SignUpSchema): Promise<AuthResponse> => {
47+
try {
48+
await auth.api.signUpEmail({
49+
body: {
50+
email,
51+
password,
52+
name: username,
53+
},
54+
});
55+
56+
return {
57+
success: true,
58+
message: "Signed up succesfully",
59+
};
60+
} catch (error) {
61+
const err = error as Error;
62+
return {
63+
success: false,
64+
message: err.message || "An unknown error occured.",
65+
};
66+
}
67+
};
68+
// #endregion
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"use client";
2+
3+
import { zodResolver } from "@hookform/resolvers/zod";
4+
import { Loader2 } from "lucide-react";
5+
import Link from "next/link";
6+
import { useRouter } from "next/navigation";
7+
import { useState } from "react";
8+
import { useForm } from "react-hook-form";
9+
import toast from "react-hot-toast";
10+
import { signIn } from "@/actions/auth.action";
11+
import { Button } from "@/components/ui/button";
12+
import {
13+
Card,
14+
CardContent,
15+
CardDescription,
16+
CardHeader,
17+
CardTitle,
18+
} from "@/components/ui/card";
19+
import {
20+
Form,
21+
FormControl,
22+
FormField,
23+
FormItem,
24+
FormLabel,
25+
FormMessage,
26+
} from "@/components/ui/form";
27+
import { Input } from "@/components/ui/input";
28+
import { authClient } from "@/lib/auth-client";
29+
import { cn } from "@/lib/utils";
30+
import {
31+
type SignInSchema,
32+
signInSchema,
33+
} from "@/lib/validations/auth.validation";
34+
35+
export function LoginForm({
36+
className,
37+
...props
38+
}: React.ComponentProps<"div">) {
39+
const router = useRouter();
40+
const [isLoading, setIsLoading] = useState(false);
41+
42+
const form = useForm<SignInSchema>({
43+
resolver: zodResolver(signInSchema),
44+
defaultValues: {
45+
email: "",
46+
password: "",
47+
},
48+
});
49+
50+
const signInWithGoogle = async () => {
51+
const { signIn: clientSignIn } = await authClient();
52+
await clientSignIn.social({
53+
provider: "google",
54+
callbackURL: "/dashboard",
55+
});
56+
};
57+
58+
async function onSubmit(values: SignInSchema) {
59+
setIsLoading(true);
60+
const { success, message } = await signIn(values);
61+
62+
if (success) {
63+
toast.success(message.toString());
64+
router.push("/dashboard");
65+
} else {
66+
toast.error(message.toString());
67+
}
68+
setIsLoading(false);
69+
}
70+
71+
return (
72+
<div className={cn("flex flex-col gap-6", className)} {...props}>
73+
<Card>
74+
<CardHeader className="text-center">
75+
<CardTitle className="text-xl">Welcome back</CardTitle>
76+
<CardDescription>
77+
Login with your Google account
78+
</CardDescription>
79+
</CardHeader>
80+
<CardContent>
81+
<Form {...form}>
82+
<form
83+
onSubmit={form.handleSubmit(onSubmit)}
84+
className="space-y-8"
85+
>
86+
<div className="grid gap-6">
87+
<Button
88+
type="button"
89+
variant="outline"
90+
className="w-full"
91+
onClick={signInWithGoogle}
92+
>
93+
<svg
94+
xmlns="http://www.w3.org/2000/svg"
95+
viewBox="0 0 24 24"
96+
>
97+
<path
98+
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
99+
fill="currentColor"
100+
/>
101+
</svg>
102+
Login with Google
103+
</Button>
104+
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
105+
<span className="bg-card text-muted-foreground relative z-10 px-2">
106+
Or continue with
107+
</span>
108+
</div>
109+
<div className="grid gap-6">
110+
<FormField
111+
control={form.control}
112+
name="email"
113+
render={({ field }) => (
114+
<FormItem>
115+
<FormLabel>Email</FormLabel>
116+
<FormControl>
117+
<Input
118+
placeholder="mail@mail.com"
119+
{...field}
120+
/>
121+
</FormControl>
122+
<FormMessage />
123+
</FormItem>
124+
)}
125+
/>
126+
<div className="flex flex-col gap-2">
127+
<FormField
128+
control={form.control}
129+
name="password"
130+
render={({ field }) => (
131+
<FormItem>
132+
<FormLabel>
133+
Password
134+
</FormLabel>
135+
<FormControl>
136+
<Input
137+
placeholder="*********"
138+
{...field}
139+
type="password"
140+
/>
141+
</FormControl>
142+
<FormMessage />
143+
</FormItem>
144+
)}
145+
/>
146+
<a
147+
href="#"
148+
className="ml-auto text-sm underline-offset-4 hover:underline"
149+
>
150+
Forgot your password?
151+
</a>
152+
</div>
153+
<Button
154+
type="submit"
155+
className="w-full"
156+
disabled={isLoading}
157+
>
158+
{isLoading ? (
159+
<>
160+
<Loader2 className="size-4 animate-spin mr-2" />
161+
Loading...
162+
</>
163+
) : (
164+
"Login"
165+
)}
166+
</Button>
167+
</div>
168+
<div className="text-center text-sm">
169+
Don&apos;t have an account?{" "}
170+
<Link
171+
href="/signup"
172+
className="underline underline-offset-4"
173+
>
174+
Sign up
175+
</Link>
176+
</div>
177+
</div>
178+
</form>
179+
</Form>
180+
</CardContent>
181+
</Card>
182+
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
183+
By clicking continue, you agree to our{" "}
184+
<a href="#">Terms of Service</a> and{" "}
185+
<a href="#">Privacy Policy</a>.
186+
</div>
187+
</div>
188+
);
189+
}

0 commit comments

Comments
 (0)