diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b17bb2d..ddc582c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -22,7 +22,8 @@ "Bash(npm run build:*)", "Bash(ls:*)", "Bash(curl:*)", - "Bash(echo \"\n\n## CSRF Debug aktiviert!\n\nBitte teste jetzt:\n1. Browser zu http://localhost:3050/create\n2. Dynamic QR Code erstellen versuchen\n3. Server-Logs zeigen jetzt [CSRF Debug] Output\n\nIch sehe dann:\n- Ob headerToken vorhanden ist\n- Ob cookieToken vorhanden ist \n- Ob sie übereinstimmen\n\n---\n\nStripe Portal 500 Error ist separates Problem:\nhttps://dashboard.stripe.com/test/settings/billing/portal\n→ Customer Portal Configuration muss erstellt werden\n\")" + "Bash(echo \"\n\n## CSRF Debug aktiviert!\n\nBitte teste jetzt:\n1. Browser zu http://localhost:3050/create\n2. Dynamic QR Code erstellen versuchen\n3. Server-Logs zeigen jetzt [CSRF Debug] Output\n\nIch sehe dann:\n- Ob headerToken vorhanden ist\n- Ob cookieToken vorhanden ist \n- Ob sie übereinstimmen\n\n---\n\nStripe Portal 500 Error ist separates Problem:\nhttps://dashboard.stripe.com/test/settings/billing/portal\n→ Customer Portal Configuration muss erstellt werden\n\")", + "Bash(pkill:*)" ], "deny": [], "ask": [] diff --git a/package-lock.json b/package-lock.json index f5b3180..d6ce0a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-i18next": "^13.5.0", + "resend": "^6.4.2", "sharp": "^0.33.1", "stripe": "^19.1.0", "tailwind-merge": "^2.2.0", @@ -1508,6 +1509,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@stripe/stripe-js": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.3.0.tgz", @@ -3441,6 +3448,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -4005,6 +4018,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -6471,6 +6490,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6690,6 +6715,32 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resend": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.4.2.tgz", + "integrity": "sha512-YnxmwneltZtjc7Xff+8ZjG1/xPLdstCiqsedgO/JxWTf7vKRAPCx6CkhQ3ZXskG0mrmf8+I5wr/wNRd8PQMUfw==", + "license": "MIT", + "dependencies": { + "svix": "1.76.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -7507,6 +7558,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svix": { + "version": "1.76.1", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.76.1.tgz", + "integrity": "sha512-CRuDWBTgYfDnBLRaZdKp9VuoPcNUq9An14c/k+4YJ15Qc5Grvf66vp0jvTltd4t7OIRj+8lM1DAgvSgvf7hdLw==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "@types/node": "^22.7.5", + "es6-promise": "^4.2.8", + "fast-sha256": "^1.3.0", + "url-parse": "^1.5.10", + "uuid": "^10.0.0" + } + }, + "node_modules/svix/node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/svix/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/tailwind-merge": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", @@ -7838,7 +7925,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -7917,6 +8003,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 442cce2..29658b1 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-i18next": "^13.5.0", + "resend": "^6.4.2", "sharp": "^0.33.1", "stripe": "^19.1.0", "tailwind-merge": "^2.2.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cd41799..f4b5156 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,6 +28,10 @@ model User { stripeCurrentPeriodEnd DateTime? plan Plan @default(FREE) + // Password reset fields + resetPasswordToken String? @unique + resetPasswordExpires DateTime? + qrCodes QRCode[] integrations Integration[] accounts Account[] diff --git a/src/app/(app)/qr/[id]/edit/page.tsx b/src/app/(app)/qr/[id]/edit/page.tsx index 8c99278..7b5c7fe 100644 --- a/src/app/(app)/qr/[id]/edit/page.tsx +++ b/src/app/(app)/qr/[id]/edit/page.tsx @@ -6,11 +6,13 @@ import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { showToast } from '@/components/ui/Toast'; +import { useCsrf } from '@/hooks/useCsrf'; export default function EditQRPage() { const router = useRouter(); const params = useParams(); const qrId = params.id as string; + const { fetchWithCsrf, loading: csrfLoading } = useCsrf(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -47,11 +49,8 @@ export default function EditQRPage() { setSaving(true); try { - const response = await fetch(`/api/qrs/${qrId}`, { + const response = await fetchWithCsrf(`/api/qrs/${qrId}`, { method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, body: JSON.stringify({ title, content, @@ -253,8 +252,9 @@ export default function EditQRPage() { diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx new file mode 100644 index 0000000..8aebe63 --- /dev/null +++ b/src/app/(auth)/forgot-password/page.tsx @@ -0,0 +1,155 @@ +'use client'; + +import React, { useState } from 'react'; +import Link from 'next/link'; +import { Card, CardContent } from '@/components/ui/Card'; +import { Input } from '@/components/ui/Input'; +import { Button } from '@/components/ui/Button'; +import { useCsrf } from '@/hooks/useCsrf'; + +export default function ForgotPasswordPage() { + const { fetchWithCsrf, loading: csrfLoading } = useCsrf(); + const [email, setEmail] = useState(''); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const response = await fetchWithCsrf('/api/auth/forgot-password', { + method: 'POST', + body: JSON.stringify({ email }), + }); + + const data = await response.json(); + + if (response.ok) { + setSuccess(true); + } else { + setError(data.error || 'Failed to send reset email'); + } + } catch (err) { + setError('An error occurred. Please try again.'); + } finally { + setLoading(false); + } + }; + + if (success) { + return ( +
We've sent you a password reset link
++ We've sent a password reset link to {email} +
+ ++ Please check your email and click the link to reset your password. The link will expire in 1 hour. +
+ +No worries, we'll send you reset instructions
++ Remember your password?{' '} + + Sign in + +
+Your password has been updated
++ Your password has been successfully reset! +
+ ++ Redirecting you to the login page in 3 seconds... +
+ + + + +Enter your new password below
+{error}
+ + + ++ Remember your password?{' '} + + Sign in + +
+- Need help?{' '} - - Visit our FAQ - {' '} - or{' '} - - read our blog - -
| + + | +