diff --git a/frontend/app/[locale]/forgot-password/page.tsx b/frontend/app/[locale]/forgot-password/page.tsx index 38dddfc5..90be6e32 100644 --- a/frontend/app/[locale]/forgot-password/page.tsx +++ b/frontend/app/[locale]/forgot-password/page.tsx @@ -1,123 +1,7 @@ "use client"; -import { Link } from "@/i18n/routing"; -import { useState } from "react"; -import { useSearchParams } from "next/navigation"; -import { Button } from "@/components/ui/button"; -export default function ForgotPasswordPage() { - const searchParams = useSearchParams(); - const returnTo = searchParams.get("returnTo"); - - const [loading, setLoading] = useState(false); - const [email, setEmail] = useState(""); - const [submitted, setSubmitted] = useState(false); - const [error, setError] = useState(null); - - async function onSubmit(e: React.FormEvent) { - e.preventDefault(); - setLoading(true); - setError(null); - - try { - const res = await fetch("/api/auth/password-reset", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email }), - }); - - if (!res.ok) { - setError("Something went wrong. Please try again."); - return; - } - - setSubmitted(true); - } catch (err) { - console.error("Password reset request failed:", err); - setError("Network error. Please check your connection and try again."); - } finally { - setLoading(false); - } - } - - return ( -
-

- Forgot password -

+import { ForgotPasswordForm } from "@/components/auth/ForgotPasswordForm"; - {submitted ? ( -
-

- If an account for{" "} - {email} exists, we’ve sent a - password reset link. -

- -

- Please check your inbox and follow the - instructions to reset your password. -

- - - Back to login - -
- ) : ( -
-

- Enter your email address and we’ll send - you a link to reset your password. -

- - setEmail(e.target.value)} - className="w-full rounded border px-3 py-2" - /> - - {error && ( -

- {error} -

- )} - - -
- )} - - {!submitted && ( -

- Remembered your password?{" "} - - Log in - -

- )} -
- ); +export default function ForgotPasswordPage() { + return ; } \ No newline at end of file diff --git a/frontend/app/[locale]/login/page.tsx b/frontend/app/[locale]/login/page.tsx index 7821c4d7..0f3509c6 100644 --- a/frontend/app/[locale]/login/page.tsx +++ b/frontend/app/[locale]/login/page.tsx @@ -1,219 +1,22 @@ "use client"; import { useLocale } from "next-intl"; -import { Link } from "@/i18n/routing"; -import { useState } from "react"; import { useSearchParams } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { OAuthButtons } from "@/components/auth/OAuthButtons"; - -function isSafeRedirectUrl(url: string): boolean { - if (!url.startsWith("/")) return false; - if (url.startsWith("//")) return false; - if (url.includes("://")) return false; - return true; -} +import { LoginForm } from "@/components/auth/LoginForm"; +import { getSafeRedirect } from "@/lib/auth/safe-redirect"; export default function LoginPage() { - const searchParams = useSearchParams(); const locale = useLocale(); + const searchParams = useSearchParams(); - const returnToParam = searchParams.get("returnTo"); - const returnTo = returnToParam ?? ""; - - const [loading, setLoading] = useState(false); - const [errorMessage, setErrorMessage] = - useState(null); - const [errorCode, setErrorCode] = - useState(null); - const [email, setEmail] = useState(""); - const [verificationSent, setVerificationSent] = - useState(false); - const [showPassword, setShowPassword] = - useState(false); - - async function onSubmit( - e: React.FormEvent - ) { - e.preventDefault(); - setLoading(true); - setErrorMessage(null); - setErrorCode(null); - setVerificationSent(false); - - const formData = new FormData(e.currentTarget); - const emailValue = String(formData.get("email") || ""); - setEmail(emailValue); - - try { - const res = await fetch("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - email: emailValue, - password: formData.get("password"), - }), - }); - - const data = await res.json().catch(() => null); - - if (!res.ok) { - setErrorCode(data?.code ?? null); - - if (data?.code === "EMAIL_NOT_VERIFIED") { - setErrorMessage( - "Your email address is not verified. Please check your inbox." - ); - } else { - setErrorMessage("Invalid email or password"); - } - return; - } - - const redirectTarget = - returnTo && isSafeRedirectUrl(returnTo) - ? returnTo - : `/${locale}/dashboard`; - - window.location.href = redirectTarget; - } catch (err) { - console.error("Login request failed:", err); - setErrorMessage( - "Network error. Please check your connection and try again." - ); - setErrorCode(null); - } finally { - setLoading(false); - } - } - - async function resendVerification() { - if (!email) return; - - await fetch("/api/auth/resend-verification", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email }), - }); - - setVerificationSent(true); - setErrorCode(null); - setErrorMessage(null); - } + const returnTo = getSafeRedirect( + searchParams.get("returnTo") + ); return ( -
-

- Log in -

- - - -
-
- - or - -
-
- -
- setEmail(e.target.value)} - /> - -
- - - -
- -
- - Forgot password? - -
- - {errorMessage && !verificationSent && ( -
-

{errorMessage}

- - {errorCode === "EMAIL_NOT_VERIFIED" && ( - - )} -
- )} - - {verificationSent && ( -
- Verification successfully sent to{" "} - {email} -
- )} - - -
- -

- Don’t have an account?{" "} - - Sign up - -

-
+ ); } \ No newline at end of file diff --git a/frontend/app/[locale]/reset-password/page.tsx b/frontend/app/[locale]/reset-password/page.tsx index 8cc0006a..a9cfbb79 100644 --- a/frontend/app/[locale]/reset-password/page.tsx +++ b/frontend/app/[locale]/reset-password/page.tsx @@ -1,126 +1,11 @@ "use client"; -export const dynamic = "force-dynamic"; - - -import { Link } from "@/i18n/routing"; -import { useState } from "react"; import { useSearchParams } from "next/navigation"; -import { Button } from "@/components/ui/button"; +import { ResetPasswordForm } from "@/components/auth/ResetPasswordForm"; export default function ResetPasswordPage() { const searchParams = useSearchParams(); - const token = searchParams.get("token"); - - const [loading, setLoading] = useState(false); - const [password, setPassword] = useState(""); - const [confirmed, setConfirmed] = useState(false); - const [error, setError] = useState(null); - - async function onSubmit(e: React.FormEvent) { - e.preventDefault(); - setError(null); - - if (!token) { - setError("Invalid or missing reset token."); - return; - } - - setLoading(true); - - try { - const res = await fetch( - "/api/auth/password-reset/confirm", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - token, - password, - }), - } - ); - - if (!res.ok) { - setError("Invalid or expired reset link."); - return; - } - - setConfirmed(true); - } catch { - setError("Network error, please try again."); - } finally { - setLoading(false); - } - } - - if (!token) { - return ( -
-
- Invalid or missing reset token. -
- - - Back to login - -
- ); - } - - return ( -
-

- Reset password -

- - {confirmed ? ( -
-

Your password has been reset successfully.

- - - Go to login - -
- ) : ( -
-

- Enter a new password for your account. -

- - setPassword(e.target.value)} - className="w-full rounded border px-3 py-2" - /> - - {error && ( -

- {error} -

- )} + const token = searchParams.get("token") ?? ""; - -
- )} -
- ); + return ; } \ No newline at end of file diff --git a/frontend/app/[locale]/signup/page.tsx b/frontend/app/[locale]/signup/page.tsx index adf141e5..e89d9294 100644 --- a/frontend/app/[locale]/signup/page.tsx +++ b/frontend/app/[locale]/signup/page.tsx @@ -1,197 +1,22 @@ "use client"; import { useLocale } from "next-intl"; -import { Link } from "@/i18n/routing"; -import { useState } from "react"; import { useSearchParams } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { OAuthButtons } from "@/components/auth/OAuthButtons"; - -/** - * Prevent open redirect vulnerabilities. - * Allows only safe relative internal paths. - */ -function isSafeRedirectUrl(url: string): boolean { - if (!url.startsWith("/")) return false; - if (url.startsWith("//")) return false; - if (url.includes("://")) return false; - return true; -} +import { SignupForm } from "@/components/auth/SignupForm"; +import { getSafeRedirect } from "@/lib/auth/safe-redirect"; export default function SignupPage() { const locale = useLocale(); const searchParams = useSearchParams(); - const returnToParam = searchParams.get("returnTo"); - const returnTo = returnToParam ?? ""; - - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [verificationRequired, setVerificationRequired] = - useState(false); - const [email, setEmail] = useState(""); - - async function onSubmit( - e: React.FormEvent - ) { - e.preventDefault(); - setLoading(true); - setError(null); - - const formData = new FormData(e.currentTarget); - const emailValue = String(formData.get("email") || ""); - setEmail(emailValue); - - let res: Response | undefined; - - try { - res = await fetch("/api/auth/signup", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: formData.get("name"), - email: emailValue, - password: formData.get("password"), - }), - }); - - const data = await res.json().catch(() => null); - - if (!res.ok) { - setError( - data?.error ?? - "Failed to sign up. Please try again." - ); - return; - } - - if (data?.verificationRequired) { - setVerificationRequired(true); - return; - } - - const redirectTarget = - returnTo && isSafeRedirectUrl(returnTo) - ? returnTo - : `/${locale}/dashboard`; - - window.location.href = redirectTarget; - } catch { - setError( - "Network error. Please check your connection and try again." - ); - } finally { - setLoading(false); - } - } + const returnTo = getSafeRedirect( + searchParams.get("returnTo") + ); return ( -
-

- Sign up -

- - {!verificationRequired && ( - <> - - -
-
- - or - -
-
- - )} - - {verificationRequired ? ( -
-

- We’ve sent a verification email to{" "} - {email}. -

- -

- Please check your inbox and click the - verification link to activate your account. -

- - - Go to login - -
- ) : ( -
- - - - - - - {error && ( -

- {error} -

- )} - - -
- )} - - {!verificationRequired && ( -

- Already have an account?{" "} - - Log in - -

- )} -
+ ); } \ No newline at end of file diff --git a/frontend/components/auth/AuthErrorBanner.tsx b/frontend/components/auth/AuthErrorBanner.tsx new file mode 100644 index 00000000..74e76c37 --- /dev/null +++ b/frontend/components/auth/AuthErrorBanner.tsx @@ -0,0 +1,29 @@ +// import type { ReactNode } from "react"; + +type AuthErrorBannerProps = { + message: string; + actionLabel?: string; + onAction?: () => void; +}; + +export function AuthErrorBanner({ + message, + actionLabel, + onAction, +}: AuthErrorBannerProps) { + return ( +
+

{message}

+ + {actionLabel && onAction && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/components/auth/AuthProvidersBlock.tsx b/frontend/components/auth/AuthProvidersBlock.tsx new file mode 100644 index 00000000..e54e3f64 --- /dev/null +++ b/frontend/components/auth/AuthProvidersBlock.tsx @@ -0,0 +1,17 @@ +import { OAuthButtons } from "@/components/auth/OAuthButtons"; + +export function AuthProvidersBlock() { + return ( + <> + + +
+
+ + or + +
+
+ + ); +} \ No newline at end of file diff --git a/frontend/components/auth/AuthShell.tsx b/frontend/components/auth/AuthShell.tsx new file mode 100644 index 00000000..1875133f --- /dev/null +++ b/frontend/components/auth/AuthShell.tsx @@ -0,0 +1,31 @@ +import type { ReactNode } from "react"; + +type AuthShellProps = { + title: string; + children: ReactNode; + footer?: ReactNode; +}; + +export function AuthShell({ + title, + children, + footer, +}: AuthShellProps) { + return ( +
+

+ {title} +

+ +
+ {children} +
+ + {footer && ( +
+ {footer} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/components/auth/AuthSuccessBanner.tsx b/frontend/components/auth/AuthSuccessBanner.tsx new file mode 100644 index 00000000..5524ce35 --- /dev/null +++ b/frontend/components/auth/AuthSuccessBanner.tsx @@ -0,0 +1,23 @@ +import type { ReactNode } from "react"; + +type AuthSuccessBannerProps = { + message: ReactNode; + footer?: ReactNode; +}; + +export function AuthSuccessBanner({ + message, + footer, +}: AuthSuccessBannerProps) { + return ( +
+
{message}
+ + {footer && ( +
+ {footer} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/components/auth/ForgotPasswordForm.tsx b/frontend/components/auth/ForgotPasswordForm.tsx new file mode 100644 index 00000000..1bf6206e --- /dev/null +++ b/frontend/components/auth/ForgotPasswordForm.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { AuthShell } from "@/components/auth/AuthShell"; +import { AuthErrorBanner } from "@/components/auth/AuthErrorBanner"; +import { AuthSuccessBanner } from "@/components/auth/AuthSuccessBanner"; +import { EmailField } from "@/components/auth/fields/EmailField"; + +export function ForgotPasswordForm() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [emailSent, setEmailSent] = useState(false); + const [email, setEmail] = useState(""); + + async function onSubmit( + e: React.FormEvent + ) { + e.preventDefault(); + setLoading(true); + setError(null); + + const formData = new FormData(e.currentTarget); + const emailValue = String(formData.get("email") || ""); + setEmail(emailValue); + + try { + const res = await fetch( + "/api/auth/password-reset", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email: emailValue }), + } + ); + + if (!res.ok) { + setError( + "Failed to send reset email. Please try again." + ); + return; + } + + setEmailSent(true); + } catch { + setError( + "Network error. Please check your connection." + ); + } finally { + setLoading(false); + } + } + + return ( + + {emailSent ? ( + +

+ We’ve sent a password reset link to{" "} + {email}. +

+

+ Please check your inbox. +

+ + } + /> + ) : ( +
+ + + {error && ( + + )} + + + + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/components/auth/LoginForm.tsx b/frontend/components/auth/LoginForm.tsx new file mode 100644 index 00000000..d6946a4f --- /dev/null +++ b/frontend/components/auth/LoginForm.tsx @@ -0,0 +1,196 @@ +"use client"; + +import { useState } from "react"; +import { Link } from "@/i18n/routing"; +import { Button } from "@/components/ui/button"; +import { AuthShell } from "@/components/auth/AuthShell"; +import { AuthProvidersBlock } from "@/components/auth/AuthProvidersBlock"; +import { EmailField } from "@/components/auth/fields/EmailField"; +import { PasswordField } from "@/components/auth/fields/PasswordField"; +import { AuthErrorBanner } from "@/components/auth/AuthErrorBanner"; +import { AuthSuccessBanner } from "@/components/auth/AuthSuccessBanner"; + + +type LoginFormProps = { + locale: string; + returnTo: string; +}; + +export function LoginForm({ + locale, + returnTo, +}: LoginFormProps) { + const [loading, setLoading] = useState(false); + const [errorMessage, setErrorMessage] = + useState(null); + const [errorCode, setErrorCode] = + useState(null); + const [email, setEmail] = useState(""); + const [verificationSent, setVerificationSent] = + useState(false); + + async function onSubmit( + e: React.FormEvent + ) { + e.preventDefault(); + setLoading(true); + setErrorMessage(null); + setErrorCode(null); + setVerificationSent(false); + + const formData = new FormData(e.currentTarget); + const emailValue = String(formData.get("email") || ""); + setEmail(emailValue); + + try { + const res = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: emailValue, + password: formData.get("password"), + }), + }); + + const data = await res.json().catch(() => null); + + if (!res.ok) { + setErrorCode(data?.code ?? null); + + if (data?.code === "EMAIL_NOT_VERIFIED") { + setErrorMessage( + "Your email address is not verified. Please check your inbox." + ); + } else { + setErrorMessage("Invalid email or password"); + } + return; + } + + window.location.href = + returnTo || `/${locale}/dashboard`; + } catch (err) { + console.error("Login request failed:", err); + setErrorMessage( + "Network error. Please check your connection and try again." + ); + setErrorCode(null); + } finally { + setLoading(false); + } + } + + async function resendVerification() { + if (!email) return; + + try { + const res = await fetch("/api/auth/resend-verification", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }); + + const data = await res.json().catch(() => null); + + if (!res.ok) { + setErrorCode(data?.code ?? "RESEND_FAILED"); + setErrorMessage( + data?.error ?? + "Failed to resend verification email. Please try again." + ); + return; + } + + setVerificationSent(true); + setErrorCode(null); + setErrorMessage(null); + } catch (err) { + console.error("Resend verification failed:", err); + setErrorCode("NETWORK_ERROR"); + setErrorMessage( + "Network error. Please check your connection and try again." + ); + } + } + + return ( + + Don’t have an account?{" "} + + Sign up + +

+ } + > + + +
+ + + + +
+ + Forgot password? + +
+ + {errorMessage && !verificationSent && ( + + )} + + {verificationSent && ( + + Verification successfully sent to{" "} + {email} + + } + /> + )} + + + +
+ ); +} \ No newline at end of file diff --git a/frontend/components/auth/ResetPasswordForm.tsx b/frontend/components/auth/ResetPasswordForm.tsx new file mode 100644 index 00000000..17dcd505 --- /dev/null +++ b/frontend/components/auth/ResetPasswordForm.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { AuthShell } from "@/components/auth/AuthShell"; +import { AuthErrorBanner } from "@/components/auth/AuthErrorBanner"; +import { AuthSuccessBanner } from "@/components/auth/AuthSuccessBanner"; +import { PasswordField } from "@/components/auth/fields/PasswordField"; + +type ResetPasswordFormProps = { + token: string; +}; + +export function ResetPasswordForm({ + token, +}: ResetPasswordFormProps) { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + async function onSubmit( + e: React.FormEvent + ) { + e.preventDefault(); + setLoading(true); + setError(null); + + const formData = new FormData(e.currentTarget); + + try { + const res = await fetch( + "/api/auth/password-reset/confirm", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + token, + password: formData.get("password"), + }), + } + ); + + if (!res.ok) { + setError( + "Failed to reset password. The link may be invalid or expired." + ); + return; + } + + setSuccess(true); + } catch { + setError( + "Network error. Please try again." + ); + } finally { + setLoading(false); + } + } + + return ( + + {success ? ( + + ) : ( +
+ + + {error && ( + + )} + + + + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/components/auth/SignupForm.tsx b/frontend/components/auth/SignupForm.tsx new file mode 100644 index 00000000..96dc4d46 --- /dev/null +++ b/frontend/components/auth/SignupForm.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { useState } from "react"; +import { Link } from "@/i18n/routing"; +import { Button } from "@/components/ui/button"; +import { AuthShell } from "@/components/auth/AuthShell"; +import { AuthProvidersBlock } from "@/components/auth/AuthProvidersBlock"; +import { EmailField } from "@/components/auth/fields/EmailField"; +import { PasswordField } from "@/components/auth/fields/PasswordField"; +import { NameField } from "@/components/auth/fields/NameField"; +import { AuthErrorBanner } from "@/components/auth/AuthErrorBanner"; +import { AuthSuccessBanner } from "@/components/auth/AuthSuccessBanner"; + +type SignupFormProps = { + locale: string; + returnTo: string; +}; + +export function SignupForm({ + locale, + returnTo, +}: SignupFormProps) { + const [loading, setLoading] = useState(false); + const [error, setError] = + useState(null); + const [verificationRequired, setVerificationRequired] = + useState(false); + const [email, setEmail] = useState(""); + + async function onSubmit( + e: React.FormEvent + ) { + e.preventDefault(); + setLoading(true); + setError(null); + + const formData = new FormData(e.currentTarget); + const emailValue = String(formData.get("email") || ""); + setEmail(emailValue); + + try { + const res = await fetch("/api/auth/signup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: formData.get("name"), + email: emailValue, + password: formData.get("password"), + }), + }); + + const data = await res.json().catch(() => null); + + if (!res.ok) { + setError( + data?.error ?? + "Failed to sign up. Please try again." + ); + return; + } + + if (data?.verificationRequired) { + setVerificationRequired(true); + return; + } + + window.location.href = + returnTo || `/${locale}/dashboard`; + } catch { + setError( + "Network error. Please check your connection and try again." + ); + } finally { + setLoading(false); + } + } + + return ( + + Already have an account?{" "} + + Log in + +

+ ) + } + > + {!verificationRequired && ( + + )} + + {verificationRequired ? ( + +

+ We’ve sent a verification email to{" "} + {email}. +

+ +

+ Please check your inbox and click the + verification link to activate your account. +

+ + } + footer={ + + Go to login + + } + /> + ) : ( +
+ + + + + + + {error && ( + + )} + + + + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/components/auth/fields/EmailField.tsx b/frontend/components/auth/fields/EmailField.tsx new file mode 100644 index 00000000..86fe4ae0 --- /dev/null +++ b/frontend/components/auth/fields/EmailField.tsx @@ -0,0 +1,22 @@ +type EmailFieldProps = { + onChange?: (value: string) => void; +}; + +export function EmailField({ + onChange, +}: EmailFieldProps) { + return ( + onChange(e.target.value) + : undefined + } + /> + ); +} \ No newline at end of file diff --git a/frontend/components/auth/fields/NameField.tsx b/frontend/components/auth/fields/NameField.tsx new file mode 100644 index 00000000..db84309c --- /dev/null +++ b/frontend/components/auth/fields/NameField.tsx @@ -0,0 +1,19 @@ +type NameFieldProps = { + name?: string; + placeholder?: string; +}; + +export function NameField({ + name = "name", + placeholder = "Name", +}: NameFieldProps) { + return ( + + ); +} \ No newline at end of file diff --git a/frontend/components/auth/fields/PasswordField.tsx b/frontend/components/auth/fields/PasswordField.tsx new file mode 100644 index 00000000..42a5743f --- /dev/null +++ b/frontend/components/auth/fields/PasswordField.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useState } from "react"; + +type PasswordFieldProps = { + name?: string; + placeholder?: string; + minLength?: number; +}; + +export function PasswordField({ + name = "password", + placeholder = "Password", + minLength, +}: PasswordFieldProps) { + const [visible, setVisible] = useState(false); + + return ( +
+ + + +
+ ); +} \ No newline at end of file diff --git a/frontend/lib/auth/safe-redirect.ts b/frontend/lib/auth/safe-redirect.ts new file mode 100644 index 00000000..e23a8b11 --- /dev/null +++ b/frontend/lib/auth/safe-redirect.ts @@ -0,0 +1,13 @@ +export function getSafeRedirect( + raw: string | null | undefined +): string { + if (!raw) return ""; + + if (raw.includes("\\")) return ""; + + if (!raw.startsWith("/")) return ""; + if (raw.startsWith("//")) return ""; + if (raw.includes("://")) return ""; + + return raw; +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3c41f9d1..06157ba8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7512,7 +7512,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, diff --git a/frontend/project-structure.txt b/frontend/project-structure.txt index df3e5f36..d0972d47 100644 --- a/frontend/project-structure.txt +++ b/frontend/project-structure.txt @@ -70,14 +70,15 @@ 📄 OrderStatusAutoRefresh.tsx 📄 page.tsx 📁 orders - 📄 error.tsx - 📄 page.tsx 📁 [id] 📄 page.tsx + 📄 error.tsx + 📄 page.tsx 📄 page.tsx 📁 products 📁 [slug] 📄 page.tsx + 📄 page.tsx 📁 signup 📄 page.tsx 📁 terms-of-service @@ -167,6 +168,7 @@ 📄 StatsSection.tsx 📁 auth 📄 OAuthButtons.tsx + 📄 PostAuthQuizSync.tsx 📄 ProviderButton.tsx 📁 icons 📄 GitHubIcon.tsx @@ -347,11 +349,13 @@ 📄 0001_add_payment_attempts.sql 📄 0002_clean_martin_li.sql 📄 0003_add_stripe_events_claim_lock.sql + 📄 0004_add_api_rate_limits.sql 📁 meta 📄 0000_snapshot.json 📄 0001_snapshot.json 📄 0002_snapshot.json 📄 0003_snapshot.json + 📄 0004_snapshot.json 📄 _journal.json 📄 drizzle.config.ts 📄 eslint.config.mjs @@ -408,6 +412,7 @@ 📁 security 📄 admin-csrf.ts 📄 csrf.ts + 📄 rate-limit.ts 📁 services 📄 errors.ts 📄 inventory.ts