From b3495b1aab19d8567616a453e664cf4f1a1b3ab8 Mon Sep 17 00:00:00 2001 From: Async Date: Tue, 14 Jul 2026 22:00:19 +0200 Subject: [PATCH 01/17] feat: add sign-in/sign-up pages with Turnstile captcha Wires Better Auth email/password sign-in and registration cards, name sanitization for display names, and Cloudflare Turnstile verification on both forms via the captcha plugin. --- bun.lock | 3 + package.json | 1 + src/app/auth/sign-in/page.tsx | 9 ++ src/app/auth/sign-up/page.tsx | 9 ++ src/components/auth/registration-card.tsx | 123 ++++++++++++++++++++ src/components/auth/sign-in-card.tsx | 100 ++++++++++++++++ src/components/ui/card.tsx | 103 ++++++++++++++++ src/components/ui/input.tsx | 20 ++++ src/components/ui/label.tsx | 20 ++++ src/lib/auth-functions/name-sanitization.ts | 3 + src/lib/auth.ts | 8 ++ 11 files changed, 399 insertions(+) create mode 100644 src/app/auth/sign-in/page.tsx create mode 100644 src/app/auth/sign-up/page.tsx create mode 100644 src/components/auth/registration-card.tsx create mode 100644 src/components/auth/sign-in-card.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/lib/auth-functions/name-sanitization.ts diff --git a/bun.lock b/bun.lock index c5c9ee6..fef1b76 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "awaketh-web", "dependencies": { "@base-ui/react": "^1.6.0", + "@marsidev/react-turnstile": "1.5.3", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", "@sentry/nextjs": "^10", @@ -305,6 +306,8 @@ "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.5.3", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-8Dij2jiNGNczq1U4EKpO4do2XepcTPxSMc2ZzvHndO+gcp68tvMULm27z2P99rGkdB89hc3452NZeu2Rti4g6A=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], diff --git a/package.json b/package.json index 65c018e..c83d700 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@base-ui/react": "^1.6.0", + "@marsidev/react-turnstile": "1.5.3", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", "@sentry/nextjs": "^10", diff --git a/src/app/auth/sign-in/page.tsx b/src/app/auth/sign-in/page.tsx new file mode 100644 index 0000000..e2a0fb6 --- /dev/null +++ b/src/app/auth/sign-in/page.tsx @@ -0,0 +1,9 @@ +import { SignInCard } from '@/components/auth/sign-in-card'; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/src/app/auth/sign-up/page.tsx b/src/app/auth/sign-up/page.tsx new file mode 100644 index 0000000..c37074f --- /dev/null +++ b/src/app/auth/sign-up/page.tsx @@ -0,0 +1,9 @@ +import { SignUpCard } from '@/components/auth/registration-card'; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/src/components/auth/registration-card.tsx b/src/components/auth/registration-card.tsx new file mode 100644 index 0000000..bf6ac07 --- /dev/null +++ b/src/components/auth/registration-card.tsx @@ -0,0 +1,123 @@ +'use client'; + +import React, { useRef } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { authClient } from '@/lib/auth-client'; +import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile'; +import { sanitizeWord } from '@/lib/auth-functions/name-sanitization'; + +export function SignUpCard() { + const tokenRef = useRef(null); + + async function onSubmit(event: React.SubmitEvent) { + event.preventDefault(); + + const formData = new FormData(event.currentTarget); + + const email = formData.get('email') as string; + const password = formData.get('password') as string; + + const firstName = formData.get('firstName') as string; + const lastName = formData.get('lastName') as string; + + const { data, error } = await authClient.signUp.email({ + name: sanitizeWord(firstName) + ' ' + sanitizeWord(lastName), + email: email, + password: password, + fetchOptions: { + headers: { + 'x-captcha-response': tokenRef.current?.getResponse(), + }, + }, + }); + + console.log(error); + console.log(data); + } + + return ( + <> + + + Awaketh account registration + + Get started with your awaketh experience. + + + + + + +
+
+
+
+ + +
+ +
+ + +
+
+
+ + +
+
+ +
+ +
+
+
+ + + + +
+ + ); +} diff --git a/src/components/auth/sign-in-card.tsx b/src/components/auth/sign-in-card.tsx new file mode 100644 index 0000000..808352a --- /dev/null +++ b/src/components/auth/sign-in-card.tsx @@ -0,0 +1,100 @@ +'use client'; + +import React, { useRef } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { authClient } from '@/lib/auth-client'; +import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile'; + +export function SignInCard() { + const tokenRef = useRef(null); + + async function onSubmit(event: React.SubmitEvent) { + event.preventDefault(); + + const formData = new FormData(event.currentTarget); + + const email = formData.get('email') as string; + const password = formData.get('password') as string; + + const { error } = await authClient.signIn.email({ + email: email, + password: password, + fetchOptions: { + headers: { + 'x-captcha-response': tokenRef.current?.getResponse(), + }, + }, + }); + + console.log(error); + } + + return ( + <> + + + Log into your Awaketh account + + Enter your email below to log into your account. + + + + + + +
+
+
+ + +
+ + +
+
+
+ + + + +
+ + ); +} diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..4458dae --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,103 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ + className, + size = "default", + ...props +}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { + return ( +
img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", + className + )} + {...props} + /> + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..7d21bab --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,20 @@ +import * as React from "react" +import { Input as InputPrimitive } from "@base-ui/react/input" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..74da65c --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,20 @@ +"use client" + +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Label({ className, ...props }: React.ComponentProps<"label">) { + return ( +