diff --git a/public/fonts/geist/mono/GeistMono-VariableFont_wght.ttf b/public/fonts/geist/mono/GeistMono-VariableFont_wght.ttf new file mode 100644 index 0000000..f86f195 Binary files /dev/null and b/public/fonts/geist/mono/GeistMono-VariableFont_wght.ttf differ diff --git a/public/fonts/geist/sans/Geist-VariableFont_wght.ttf b/public/fonts/geist/sans/Geist-VariableFont_wght.ttf new file mode 100644 index 0000000..ad6f2c5 Binary files /dev/null and b/public/fonts/geist/sans/Geist-VariableFont_wght.ttf differ diff --git a/src/app/(legal)/privacy/page.tsx b/src/app/(legal)/privacy/page.tsx new file mode 100644 index 0000000..7544b8d --- /dev/null +++ b/src/app/(legal)/privacy/page.tsx @@ -0,0 +1,390 @@ +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Button } from "~/components/ui/button"; +import Link from "next/link"; +import { ArrowLeft } from "lucide-react"; + +export default function PrivacyPolicyPage() { + return ( +
+ {/* Header */} +
+
+
+ + + +
+

Privacy Policy

+

+ Last updated: {new Date().toLocaleDateString()} +

+
+
+
+
+ + {/* Content */} +
+
+ + + Introduction + + +

+ beenvoice ("we", "our", or "us") + is committed to protecting your privacy. This Privacy Policy + explains how we collect, use, disclose, and safeguard your + information when you use our invoicing platform and services. +

+

+ Please read this Privacy Policy carefully. If you do not agree + with the terms of this Privacy Policy, please do not access or + use our Service. +

+
+
+ + + + Information We Collect + + +

Personal Information

+

+ We may collect personal information that you voluntarily provide + to us when you: +

+
    +
  • Register for an account
  • +
  • Create invoices or manage client information
  • +
  • Contact us for support
  • +
  • Subscribe to our newsletters or communications
  • +
+ +

This personal information may include:

+
    +
  • Name and contact information (email, phone, address)
  • +
  • Business information and tax details
  • +
  • Client information you input into the system
  • +
  • Financial information related to your invoices
  • +
  • + Payment information (processed securely by third-party + providers) +
  • +
+ +

Automatically Collected Information

+

+ We may automatically collect certain information when you visit + our Service: +

+
    +
  • + Device information (IP address, browser type, operating + system) +
  • +
  • Usage data (pages visited, time spent, features used)
  • +
  • Log files and analytics data
  • +
  • Cookies and similar tracking technologies
  • +
+
+
+ + + + How We Use Your Information + + +

We use the information we collect to:

+
    +
  • Provide, operate, and maintain our Service
  • +
  • Process your transactions and manage your account
  • +
  • Improve and personalize your experience
  • +
  • + Communicate with you about your account and our services +
  • +
  • Send you technical notices and support messages
  • +
  • Respond to your comments, questions, and requests
  • +
  • Monitor usage and analyze trends
  • +
  • + Detect, prevent, and address technical issues and security + breaches +
  • +
  • Comply with legal obligations
  • +
+
+
+ + + + How We Share Your Information + + +

+ We do not sell, trade, or rent your personal information to + third parties. We may share your information in the following + circumstances: +

+ +

Service Providers

+

+ We may share your information with trusted third-party service + providers who assist us in operating our Service, such as: +

+
    +
  • Cloud hosting and storage providers
  • +
  • Payment processors
  • +
  • Email service providers
  • +
  • Analytics and monitoring services
  • +
+ +

Legal Requirements

+

+ We may disclose your information if required to do so by law or + in response to: +

+
    +
  • Legal processes (subpoenas, court orders)
  • +
  • Government requests
  • +
  • Law enforcement investigations
  • +
  • Protection of our rights, property, or safety
  • +
+ +

Business Transfers

+

+ In the event of a merger, acquisition, or sale of assets, your + information may be transferred as part of that transaction. +

+
+
+ + + + Data Security + + +

+ We implement appropriate technical and organizational security + measures to protect your information: +

+
    +
  • Encryption of data in transit and at rest
  • +
  • Secure access controls and authentication
  • +
  • Regular security assessments and updates
  • +
  • Employee training on data protection
  • +
  • Incident response procedures
  • +
+

+ However, no method of transmission over the internet or + electronic storage is 100% secure. While we strive to protect + your information, we cannot guarantee absolute security. +

+
+
+ + + + Data Retention + + +

+ We retain your personal information only for as long as + necessary to fulfill the purposes outlined in this Privacy + Policy, unless a longer retention period is required by law. +

+

+ Factors we consider when determining retention periods include: +

+
    +
  • The nature and sensitivity of the information
  • +
  • Legal and regulatory requirements
  • +
  • Business and operational needs
  • +
  • Your account status and activity
  • +
+
+
+ + + + Your Rights and Choices + + +

+ Depending on your location, you may have the following rights + regarding your personal information: +

+ +

Access and Portability

+
    +
  • Request access to your personal information
  • +
  • Receive a copy of your data in a portable format
  • +
+ +

Correction and Updates

+
    +
  • Correct inaccurate or incomplete information
  • +
  • Update your account information at any time
  • +
+ +

Deletion

+
    +
  • Request deletion of your personal information
  • +
  • Close your account and remove your data
  • +
+ +

Restriction and Objection

+
    +
  • Restrict the processing of your information
  • +
  • Object to certain uses of your data
  • +
+ +

+ To exercise these rights, please contact us using the + information provided in the "Contact Us" section + below. +

+
+
+ + + + Cookies and Tracking Technologies + + +

We use cookies and similar technologies to:

+
    +
  • Remember your preferences and settings
  • +
  • Authenticate your account
  • +
  • Analyze usage patterns and improve our Service
  • +
  • Provide personalized content and features
  • +
+ +

+ You can control cookies through your browser settings. However, + disabling cookies may affect the functionality of our Service. +

+ +

Types of Cookies We Use

+
    +
  • + Essential Cookies: Required for the Service + to function properly +
  • +
  • + Analytics Cookies: Help us understand how you + use our Service +
  • +
  • + Preference Cookies: Remember your settings + and preferences +
  • +
+
+
+ + + + Third-Party Links and Services + + +

+ Our Service may contain links to third-party websites or + integrate with third-party services. We are not responsible for + the privacy practices of these third parties. +

+

+ We encourage you to read the privacy policies of any third-party + services you use in connection with our Service. +

+
+
+ + + + Children's Privacy + + +

+ Our Service is not intended for children under the age of 13. We + do not knowingly collect personal information from children + under 13. +

+

+ If you are a parent or guardian and believe your child has + provided us with personal information, please contact us + immediately so we can remove such information. +

+
+
+ + + + International Data Transfers + + +

+ Your information may be transferred to and processed in + countries other than your own. We ensure that such transfers + comply with applicable data protection laws. +

+

+ When we transfer your information internationally, we implement + appropriate safeguards to protect your data, including: +

+
    +
  • Standard contractual clauses
  • +
  • Adequacy decisions by relevant authorities
  • +
  • Certified privacy frameworks
  • +
+
+
+ + + + Changes to This Privacy Policy + + +

+ We may update this Privacy Policy from time to time. We will + notify you of any material changes by: +

+
    +
  • Posting the updated policy on our Service
  • +
  • Sending you an email notification
  • +
  • Displaying a prominent notice on our Service
  • +
+

+ Your continued use of our Service after any changes indicates + your acceptance of the updated Privacy Policy. +

+
+
+ + + + Contact Us + + +

+ If you have questions about this Privacy Policy or our privacy + practices, please contact us at: +

+
    +
  • Email: privacy@beenvoice.com
  • +
  • Address: [Your Business Address]
  • +
+

+ We will respond to your inquiries within a reasonable timeframe + and in accordance with applicable law. +

+
+
+
+
+
+ ); +} diff --git a/src/app/(legal)/terms/page.tsx b/src/app/(legal)/terms/page.tsx new file mode 100644 index 0000000..e1786e7 --- /dev/null +++ b/src/app/(legal)/terms/page.tsx @@ -0,0 +1,306 @@ +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Button } from "~/components/ui/button"; +import Link from "next/link"; +import { ArrowLeft } from "lucide-react"; + +export default function TermsOfServicePage() { + return ( +
+ {/* Header */} +
+
+
+ + + +
+

Terms of Service

+

+ Last updated: {new Date().toLocaleDateString()} +

+
+
+
+
+ + {/* Content */} +
+
+ + + Agreement to Terms + + +

+ These Terms of Service ("Terms") govern your use of the + beenvoice platform and services (the "Service") operated by + beenvoice ("us", "we", or "our"). +

+

+ By accessing or using our Service, you agree to be bound by + these Terms. If you disagree with any part of these terms, then + you may not access the Service. +

+
+
+ + + + Description of Service + + +

+ beenvoice is a web-based invoicing platform that allows users + to: +

+
    +
  • Create and manage professional invoices
  • +
  • Track client information and billing details
  • +
  • Monitor payment status and financial metrics
  • +
  • Generate reports and analytics
  • +
  • Manage business profiles and settings
  • +
+
+
+ + + + User Accounts + + +

+ When you create an account with us, you must provide information + that is accurate, complete, and current at all times. You are + responsible for safeguarding the password and for all activities + that occur under your account. +

+

+ You agree not to disclose your password to any third party. You + must notify us immediately upon becoming aware of any breach of + security or unauthorized use of your account. +

+
+
+ + + + Acceptable Use + + +

You agree not to use the Service:

+
    +
  • + For any unlawful purpose or to solicit others to perform + unlawful acts +
  • +
  • + To violate any international, federal, provincial, or state + regulations, rules, laws, or local ordinances +
  • +
  • + To infringe upon or violate our intellectual property rights + or the intellectual property rights of others +
  • +
  • + To harass, abuse, insult, harm, defame, slander, disparage, + intimidate, or discriminate +
  • +
  • To submit false or misleading information
  • +
  • + To upload or transmit viruses or any other type of malicious + code +
  • +
  • + To spam, phish, pharm, pretext, spider, crawl, or scrape +
  • +
  • For any obscene or immoral purpose
  • +
  • + To interfere with or circumvent the security features of the + Service +
  • +
+
+
+ + + + Data and Privacy + + +

+ Your privacy is important to us. Please review our Privacy + Policy, which also governs your use of the Service, to + understand our practices. +

+

+ You retain ownership of your data. We will not sell, rent, or + share your personal information with third parties without your + explicit consent, except as described in our Privacy Policy. +

+

+ You are responsible for backing up your data. While we implement + regular backups, we recommend you maintain your own copies of + important information. +

+
+
+ + + + Payment Terms + + +

+ Some aspects of the Service may require payment. You will be + charged according to your subscription plan. All fees are + non-refundable unless otherwise stated. +

+

+ We may change our fees at any time. We will provide you with + reasonable notice of any fee changes by posting the new fees on + the Service or sending you email notification. +

+

+ If you fail to pay any fees when due, we may suspend or + terminate your access to the Service until payment is made. +

+
+
+ + + + Intellectual Property Rights + + +

+ The Service and its original content, features, and + functionality are and will remain the exclusive property of + beenvoice and its licensors. The Service is protected by + copyright, trademark, and other laws. +

+

+ Our trademarks and trade dress may not be used in connection + with any product or service without our prior written consent. +

+
+
+ + + + Termination + + +

+ We may terminate or suspend your account and bar access to the + Service immediately, without prior notice or liability, under + our sole discretion, for any reason whatsoever and without + limitation, including but not limited to a breach of the Terms. +

+

+ If you wish to terminate your account, you may simply + discontinue using the Service and contact us to request account + deletion. +

+

+ Upon termination, your right to use the Service will cease + immediately. If you wish to terminate your account, you may + simply discontinue using the Service. +

+
+
+ + + + Disclaimer of Warranties + + +

+ The information on this Service is provided on an "as + is" basis. To the fullest extent permitted by law, we + exclude all representations, warranties, and conditions relating + to our Service and the use of this Service. +

+

+ Nothing in this disclaimer will limit or exclude our or your + liability for death or personal injury resulting from + negligence, fraud, or fraudulent misrepresentation. +

+
+
+ + + + Limitation of Liability + + +

+ In no event shall beenvoice, nor its directors, employees, + partners, agents, suppliers, or affiliates, be liable for any + indirect, incidental, special, consequential, or punitive + damages, including without limitation, loss of profits, data, + use, goodwill, or other intangible losses, resulting from your + use of the Service. +

+
+
+ + + + Governing Law + + +

+ These Terms shall be interpreted and governed by the laws of the + jurisdiction in which beenvoice operates, without regard to its + conflict of law provisions. +

+

+ Our failure to enforce any right or provision of these Terms + will not be considered a waiver of those rights. +

+
+
+ + + + Changes to Terms + + +

+ We reserve the right, at our sole discretion, to modify or + replace these Terms at any time. If a revision is material, we + will provide at least 30 days notice prior to any new terms + taking effect. +

+

+ What constitutes a material change will be determined at our + sole discretion. By continuing to access or use our Service + after any revisions become effective, you agree to be bound by + the revised terms. +

+
+
+ + + + Contact Information + + +

+ If you have any questions about these Terms of Service, please + contact us at: +

+
    +
  • Email: legal@beenvoice.com
  • +
  • Address: [Your Business Address]
  • +
+
+
+
+
+
+ ); +} diff --git a/src/app/api/auth/forgot-password/route.ts b/src/app/api/auth/forgot-password/route.ts new file mode 100644 index 0000000..7109247 --- /dev/null +++ b/src/app/api/auth/forgot-password/route.ts @@ -0,0 +1,101 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { eq } from "drizzle-orm"; +import { db } from "~/server/db"; +import { users } from "~/server/db/schema"; +import { Resend } from "resend"; +import { env } from "~/env"; +import { generatePasswordResetEmailTemplate } from "~/lib/email-templates"; +import crypto from "crypto"; + +export async function POST(request: NextRequest) { + try { + const { email } = (await request.json()) as { email: string }; + + if (!email || typeof email !== "string") { + return NextResponse.json({ error: "Email is required" }, { status: 400 }); + } + + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return NextResponse.json( + { error: "Invalid email format" }, + { status: 400 }, + ); + } + + // Check if user exists + const user = await db.query.users.findFirst({ + where: eq(users.email, email.toLowerCase()), + }); + + // Always return success to prevent email enumeration attacks + // Don't reveal whether the user exists or not + if (!user) { + return NextResponse.json( + { + success: true, + message: + "If an account with that email exists, password reset instructions have been sent.", + }, + { status: 200 }, + ); + } + + // Generate reset token + const resetToken = crypto.randomBytes(32).toString("hex"); + const resetTokenExpiry = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours + + // Update user with reset token + await db + .update(users) + .set({ + resetToken, + resetTokenExpiry, + }) + .where(eq(users.id, user.id)); + + // Send password reset email using Resend + try { + const resend = new Resend(env.RESEND_API_KEY); + const resetUrl = `${process.env.NEXTAUTH_URL ?? "http://localhost:3000"}/auth/reset-password?token=${resetToken}`; + + const emailTemplate = generatePasswordResetEmailTemplate({ + userEmail: email, + userName: user.name ?? undefined, + resetToken, + resetUrl, + expiryHours: 24, + }); + + await resend.emails.send({ + from: "beenvoice ", + to: email, + subject: emailTemplate.subject, + html: emailTemplate.html, + text: emailTemplate.text, + }); + + console.log(`Password reset email sent to: ${email}`); + } catch (emailError) { + console.error("Failed to send password reset email:", emailError); + // Continue execution - don't fail the request if email fails + // This prevents revealing whether an account exists based on email delivery + } + + return NextResponse.json( + { + success: true, + message: + "If an account with that email exists, password reset instructions have been sent.", + }, + { status: 200 }, + ); + } catch (error) { + console.error("Password reset error:", error); + return NextResponse.json( + { error: "An error occurred while processing your request" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/auth/reset-password/route.ts b/src/app/api/auth/reset-password/route.ts new file mode 100644 index 0000000..a966cc1 --- /dev/null +++ b/src/app/api/auth/reset-password/route.ts @@ -0,0 +1,74 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { eq, and, gt } from "drizzle-orm"; +import bcrypt from "bcryptjs"; +import { db } from "~/server/db"; +import { users } from "~/server/db/schema"; + +export async function POST(request: NextRequest) { + try { + const { token, password } = (await request.json()) as { + token: string; + password: string; + }; + + if (!token || typeof token !== "string") { + return NextResponse.json({ error: "Token is required" }, { status: 400 }); + } + + if (!password || typeof password !== "string") { + return NextResponse.json( + { error: "Password is required" }, + { status: 400 }, + ); + } + + if (password.length < 8) { + return NextResponse.json( + { error: "Password must be at least 8 characters long" }, + { status: 400 }, + ); + } + + // Find user with valid reset token that hasn't expired + const user = await db.query.users.findFirst({ + where: and( + eq(users.resetToken, token), + gt(users.resetTokenExpiry, new Date()), + ), + }); + + if (!user) { + return NextResponse.json( + { error: "Invalid or expired token" }, + { status: 400 }, + ); + } + + // Hash the new password + const hashedPassword = await bcrypt.hash(password, 12); + + // Update user with new password and clear reset token + await db + .update(users) + .set({ + password: hashedPassword, + resetToken: null, + resetTokenExpiry: null, + }) + .where(eq(users.id, user.id)); + + return NextResponse.json( + { + success: true, + message: "Password has been reset successfully", + }, + { status: 200 }, + ); + } catch (error) { + console.error("Password reset error:", error); + return NextResponse.json( + { error: "An error occurred while resetting your password" }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/auth/validate-reset-token/route.ts b/src/app/api/auth/validate-reset-token/route.ts new file mode 100644 index 0000000..4609646 --- /dev/null +++ b/src/app/api/auth/validate-reset-token/route.ts @@ -0,0 +1,37 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { eq, and, gt } from "drizzle-orm"; +import { db } from "~/server/db"; +import { users } from "~/server/db/schema"; + +export async function POST(request: NextRequest) { + try { + const { token } = (await request.json()) as { token: string }; + + if (!token || typeof token !== "string") { + return NextResponse.json({ error: "Token is required" }, { status: 400 }); + } + + // Find user with valid reset token that hasn't expired + const user = await db.query.users.findFirst({ + where: and( + eq(users.resetToken, token), + gt(users.resetTokenExpiry, new Date()), + ), + }); + + if (!user) { + return NextResponse.json( + { error: "Invalid or expired token" }, + { status: 400 }, + ); + } + + return NextResponse.json({ valid: true }, { status: 200 }); + } catch (error) { + console.error("Token validation error:", error); + return NextResponse.json( + { error: "An error occurred while validating the token" }, + { status: 500 }, + ); + } +} diff --git a/src/app/auth/forgot-password/page.tsx b/src/app/auth/forgot-password/page.tsx new file mode 100644 index 0000000..8c9c290 --- /dev/null +++ b/src/app/auth/forgot-password/page.tsx @@ -0,0 +1,385 @@ +"use client"; + +import { useState, Suspense } from "react"; +import { Card, CardContent } from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Button } from "~/components/ui/button"; +import { Label } from "~/components/ui/label"; +import { toast } from "sonner"; +import { Logo } from "~/components/branding/logo"; +import { LegalModal } from "~/components/ui/legal-modal"; +import { + Mail, + ArrowRight, + ArrowLeft, + Shield, + Clock, + CheckCircle, +} from "lucide-react"; + +function ForgotPasswordForm() { + const [email, setEmail] = useState(""); + const [loading, setLoading] = useState(false); + const [sent, setSent] = useState(false); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + + try { + const response = await fetch("/api/auth/forgot-password", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email }), + }); + + const data = (await response.json()) as { error?: string }; + + if (response.ok) { + setSent(true); + toast.success("Password reset instructions sent to your email"); + } else { + toast.error(data.error ?? "Failed to send reset email"); + } + } catch { + toast.error("An error occurred. Please try again."); + } finally { + setLoading(false); + } + } + + if (sent) { + return ( +
+ + + {/* Hero Section - Hidden on mobile */} +
+
+
+ +
+

+ Check your + email inbox +

+

+ We've sent password reset instructions to your email + address. Follow the link to create a new password. +

+
+
+ +
+
+
+ +
+
+

Check your inbox

+

+ Look for an email from beenvoice with reset instructions +

+
+
+ +
+
+ +
+
+

Link expires soon

+

+ The reset link is valid for 24 hours only +

+
+
+ +
+
+ +
+
+

Secure Process

+

+ Your account security is our top priority +

+
+
+
+ +
+ +
+

Email sent successfully

+

+ Follow the instructions in your email to reset your + password +

+
+
+
+
+ + {/* Success Message */} +
+
+ {/* Mobile Logo */} +
+ +
+ +
+
+ +
+

Check your email

+

+ We've sent password reset instructions to{" "} + {email} +

+
+ +
+

What's next?

+
    +
  • + 1. + Check your email inbox (and spam folder) +
  • +
  • + 2. + Click the reset link in the email +
  • +
  • + 3. + Create a new secure password +
  • +
+
+ +
+ + + + + +
+ +
+ Didn't receive the email? Check your spam folder or{" "} + + . +
+
+
+
+
+
+ ); + } + + return ( +
+ + + {/* Hero Section - Hidden on mobile */} +
+
+
+ +
+

+ Forgot your + password? +

+

+ No worries! Enter your email address and we'll send you + instructions to reset your password. +

+
+
+ +
+
+
+ +
+
+

Email Instructions

+

+ We'll send a secure link to your email address +

+
+
+ +
+
+ +
+
+

Quick Process

+

+ Reset your password in just a few clicks +

+
+
+ +
+
+ +
+
+

Secure & Safe

+

+ Your account security is our top priority +

+
+
+
+
+
+ + {/* Forgot Password Form */} +
+
+ {/* Mobile Logo */} +
+ +
+ +
+

Forgot Password

+

+ Enter your email and we'll send you reset instructions +

+
+ +
+
+ +
+ + setEmail(e.target.value)} + required + autoFocus + className="h-11 pl-10" + placeholder="Enter your email address" + /> +
+
+ + +
+ +
+
+ +
+

Check your spam folder

+

+ Sometimes our emails end up in spam or promotions folders +

+
+
+
+ + + +
+ Remember your password?{" "} + + Sign in instead + +
+ +
+ By using our service, you agree to our{" "} + + Terms of Service + + } + />{" "} + and{" "} + + Privacy Policy + + } + /> + . +
+
+
+
+
+
+ ); +} + +export default function ForgotPasswordPage() { + return ( + Loading...}> + + + ); +} diff --git a/src/app/auth/reset-password/page.tsx b/src/app/auth/reset-password/page.tsx new file mode 100644 index 0000000..74614c8 --- /dev/null +++ b/src/app/auth/reset-password/page.tsx @@ -0,0 +1,462 @@ +"use client"; + +import { useState, Suspense, useEffect } from "react"; +import { useSearchParams } from "next/navigation"; +import { Card, CardContent } from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Button } from "~/components/ui/button"; +import { Label } from "~/components/ui/label"; +import { toast } from "sonner"; +import { Logo } from "~/components/branding/logo"; +import { LegalModal } from "~/components/ui/legal-modal"; +import { + Lock, + ArrowRight, + ArrowLeft, + CheckCircle, + Shield, + Eye, + EyeOff, +} from "lucide-react"; + +function ResetPasswordForm() { + const searchParams = useSearchParams(); + const token = searchParams.get("token"); + + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [tokenValid, setTokenValid] = useState(null); + + useEffect(() => { + if (!token) { + setTokenValid(false); + return; + } + + // Validate token on page load + const validateToken = async () => { + try { + const response = await fetch("/api/auth/validate-reset-token", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token }), + }); + + if (response.ok) { + setTokenValid(true); + } else { + setTokenValid(false); + } + } catch { + setTokenValid(false); + } + }; + + void validateToken(); + }, [token]); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + if (!token) { + toast.error("Invalid reset token"); + return; + } + + if (password.length < 8) { + toast.error("Password must be at least 8 characters long"); + return; + } + + if (password !== confirmPassword) { + toast.error("Passwords do not match"); + return; + } + + setLoading(true); + + try { + const response = await fetch("/api/auth/reset-password", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ token, password }), + }); + + const data = (await response.json()) as { error?: string }; + + if (response.ok) { + setSuccess(true); + toast.success("Password reset successfully!"); + } else { + toast.error(data.error ?? "Failed to reset password"); + } + } catch { + toast.error("An error occurred. Please try again."); + } finally { + setLoading(false); + } + } + + if (tokenValid === null) { + return ( +
+
+
+

+ Validating reset token... +

+
+
+ ); + } + + if (tokenValid === false) { + return ( +
+ + + {/* Hero Section - Hidden on mobile */} +
+
+
+ +
+

+ Invalid or + expired link +

+

+ This password reset link is either invalid or has expired. + Please request a new password reset. +

+
+
+ +
+
+
+ +
+
+

Security First

+

+ Reset links expire after 24 hours for your security +

+
+
+
+
+
+ + {/* Error Form */} +
+
+ {/* Mobile Logo */} +
+ +
+ +
+
+ +
+

Link Expired

+

+ This password reset link is no longer valid +

+
+ + +
+
+
+
+
+ ); + } + + if (success) { + return ( +
+ + + {/* Hero Section - Hidden on mobile */} +
+
+
+ +
+

+ Password + reset complete +

+

+ Your password has been successfully reset. You can now + sign in with your new password. +

+
+
+ +
+
+ +
+

Security Updated

+

+ Your account is now secured with your new password +

+
+
+
+
+
+ + {/* Success Form */} +
+
+ {/* Mobile Logo */} +
+ +
+ +
+
+ +
+

+ Password Reset Complete +

+

+ Your password has been successfully updated +

+
+ + +
+
+
+
+
+ ); + } + + return ( +
+ + + {/* Hero Section - Hidden on mobile */} +
+
+
+ +
+

+ Create your + new password +

+

+ Choose a strong password to secure your beenvoice account. + Make sure it's something you'll remember. +

+
+
+ +
+
+
+ +
+
+

Secure Password

+

+ Use at least 8 characters with a mix of letters and + numbers +

+
+
+ +
+
+ +
+
+

Account Safety

+

+ Your new password will immediately secure your account +

+
+
+
+
+
+ + {/* Reset Password Form */} +
+
+ {/* Mobile Logo */} +
+ +
+ +
+

Reset Password

+

+ Enter your new password below +

+
+ +
+
+ +
+ + setPassword(e.target.value)} + required + autoFocus + className="h-11 pr-10 pl-10" + placeholder="Enter new password" + minLength={8} + /> + +
+

+ Must be at least 8 characters long +

+
+ +
+ +
+ + setConfirmPassword(e.target.value)} + required + className="h-11 pr-10 pl-10" + placeholder="Confirm new password" + /> + +
+
+ + +
+ + + +
+ By resetting your password, you agree to our{" "} + + Terms of Service + + } + />{" "} + and{" "} + + Privacy Policy + + } + /> + . +
+
+
+
+
+
+ ); +} + +export default function ResetPasswordPage() { + return ( + Loading...}> + + + ); +} diff --git a/src/app/dashboard/_components/invoice-status-chart.tsx b/src/app/dashboard/_components/invoice-status-chart.tsx new file mode 100644 index 0000000..57ba83c --- /dev/null +++ b/src/app/dashboard/_components/invoice-status-chart.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; +import { getEffectiveInvoiceStatus } from "~/lib/invoice-status"; +import type { StoredInvoiceStatus } from "~/types/invoice"; + +interface Invoice { + id: string; + totalAmount: number; + status: string; + dueDate: Date | string; +} + +interface InvoiceStatusChartProps { + invoices: Invoice[]; +} + +export function InvoiceStatusChart({ invoices }: InvoiceStatusChartProps) { + // Process invoice data to create status breakdown + const statusData = invoices.reduce( + (acc, invoice) => { + const effectiveStatus = getEffectiveInvoiceStatus( + invoice.status as StoredInvoiceStatus, + invoice.dueDate, + ); + + acc[effectiveStatus] ??= { + status: effectiveStatus, + count: 0, + value: 0, + }; + + acc[effectiveStatus].count += 1; + acc[effectiveStatus].value += invoice.totalAmount; + + return acc; + }, + {} as Record, + ); + + const chartData = Object.values(statusData).map((item) => ({ + ...item, + name: item.status.charAt(0).toUpperCase() + item.status.slice(1), + })); + + // Light pastel colors for different statuses + const COLORS = { + draft: "hsl(220 9% 46%)", // muted gray + sent: "hsl(210 40% 70%)", // light blue + paid: "hsl(142 76% 85%)", // light green + overdue: "hsl(0 84% 85%)", // light red + }; + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); + }; + + const CustomTooltip = ({ + active, + payload, + }: { + active?: boolean; + payload?: Array<{ + payload: { name: string; count: number; value: number }; + }>; + }) => { + if (active && payload?.length) { + const data = payload[0]!.payload; + return ( +
+

{data.name}

+

+ {data.count} invoice{data.count !== 1 ? "s" : ""} +

+

{formatCurrency(data.value)}

+
+ ); + } + return null; + }; + + if (chartData.length === 0) { + return ( +
+
+

+ No invoice data available +

+

+ Status breakdown will appear here once you create invoices +

+
+
+ ); + } + + return ( +
+
+ + + + {chartData.map((entry, index) => ( + + ))} + + } /> + + +
+ + {/* Legend */} +
+ {chartData.map((item) => ( +
+
+
+ {item.name} +
+
+

{item.count}

+

+ {formatCurrency(item.value)} +

+
+
+ ))} +
+
+ ); +} diff --git a/src/app/dashboard/_components/monthly-metrics-chart.tsx b/src/app/dashboard/_components/monthly-metrics-chart.tsx new file mode 100644 index 0000000..63ff6be --- /dev/null +++ b/src/app/dashboard/_components/monthly-metrics-chart.tsx @@ -0,0 +1,206 @@ +"use client"; + +import { + Bar, + BarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { getEffectiveInvoiceStatus } from "~/lib/invoice-status"; +import type { StoredInvoiceStatus } from "~/types/invoice"; + +interface Invoice { + id: string; + totalAmount: number; + issueDate: Date | string; + status: string; + dueDate: Date | string; +} + +interface MonthlyMetricsChartProps { + invoices: Invoice[]; +} + +export function MonthlyMetricsChart({ invoices }: MonthlyMetricsChartProps) { + // Process invoice data to create monthly metrics + const monthlyData = invoices.reduce( + (acc, invoice) => { + const date = new Date(invoice.issueDate); + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`; + const effectiveStatus = getEffectiveInvoiceStatus( + invoice.status as StoredInvoiceStatus, + invoice.dueDate, + ); + + acc[monthKey] ??= { + month: monthKey, + totalInvoices: 0, + paidInvoices: 0, + pendingInvoices: 0, + overdueInvoices: 0, + }; + + acc[monthKey].totalInvoices += 1; + + switch (effectiveStatus) { + case "paid": + acc[monthKey].paidInvoices += 1; + break; + case "sent": + acc[monthKey].pendingInvoices += 1; + break; + case "overdue": + acc[monthKey].overdueInvoices += 1; + break; + } + + return acc; + }, + {} as Record< + string, + { + month: string; + totalInvoices: number; + paidInvoices: number; + pendingInvoices: number; + overdueInvoices: number; + } + >, + ); + + // Convert to array and sort by month + const chartData = Object.values(monthlyData) + .sort((a, b) => a.month.localeCompare(b.month)) + .slice(-6) // Show last 6 months + .map((item) => ({ + ...item, + monthLabel: new Date(item.month + "-01").toLocaleDateString("en-US", { + month: "short", + year: "2-digit", + }), + })); + + const CustomTooltip = ({ + active, + payload, + label, + }: { + active?: boolean; + payload?: Array<{ + payload: { + paidInvoices: number; + pendingInvoices: number; + overdueInvoices: number; + totalInvoices: number; + }; + }>; + label?: string; + }) => { + if (active && payload?.length) { + const data = payload[0]!.payload; + return ( +
+

{label}

+
+

+ Paid: {data.paidInvoices} +

+

+ Pending: {data.pendingInvoices} +

+

+ Overdue: {data.overdueInvoices} +

+

+ Total: {data.totalInvoices} +

+
+
+ ); + } + return null; + }; + + if (chartData.length === 0) { + return ( +
+
+

+ No metrics data available +

+

+ Monthly metrics will appear here once you create invoices +

+
+
+ ); + } + + return ( +
+
+ + + + + } /> + + + + + +
+ + {/* Legend */} +
+
+
+ Paid +
+
+
+ Pending +
+
+
+ Overdue +
+
+
+ ); +} diff --git a/src/app/dashboard/_components/revenue-chart.tsx b/src/app/dashboard/_components/revenue-chart.tsx new file mode 100644 index 0000000..c576fb4 --- /dev/null +++ b/src/app/dashboard/_components/revenue-chart.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { + Area, + AreaChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { getEffectiveInvoiceStatus } from "~/lib/invoice-status"; +import type { StoredInvoiceStatus } from "~/types/invoice"; + +interface Invoice { + id: string; + totalAmount: number; + issueDate: Date | string; + status: string; + dueDate: Date | string; +} + +interface RevenueChartProps { + invoices: Invoice[]; +} + +export function RevenueChart({ invoices }: RevenueChartProps) { + // Process invoice data to create monthly revenue data + const monthlyData = invoices + .filter( + (invoice) => + getEffectiveInvoiceStatus( + invoice.status as StoredInvoiceStatus, + invoice.dueDate, + ) === "paid", + ) + .reduce( + (acc, invoice) => { + const date = new Date(invoice.issueDate); + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`; + + acc[monthKey] ??= { + month: monthKey, + revenue: 0, + count: 0, + }; + + acc[monthKey].revenue += invoice.totalAmount; + acc[monthKey].count += 1; + + return acc; + }, + {} as Record, + ); + + // Convert to array and sort by month + const chartData = Object.values(monthlyData) + .sort((a, b) => a.month.localeCompare(b.month)) + .slice(-6) // Show last 6 months + .map((item) => ({ + ...item, + monthLabel: new Date(item.month + "-01").toLocaleDateString("en-US", { + month: "short", + year: "2-digit", + }), + })); + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); + }; + + const CustomTooltip = ({ + active, + payload, + label, + }: { + active?: boolean; + payload?: Array<{ payload: { revenue: number; count: number } }>; + label?: string; + }) => { + if (active && payload?.length) { + const data = payload[0]!.payload; + return ( +
+

{label}

+

+ Revenue: {formatCurrency(data.revenue)} +

+

+ {data.count} invoice{data.count !== 1 ? "s" : ""} +

+
+ ); + } + return null; + }; + + if (chartData.length === 0) { + return ( +
+
+

+ No revenue data available +

+

+ Revenue will appear here once you have paid invoices +

+
+
+ ); + } + + return ( +
+ + + + + + + + + + + } /> + + + +
+ ); +} diff --git a/src/app/dashboard/invoices/[id]/edit/page.tsx b/src/app/dashboard/invoices/[id]/edit/page.tsx new file mode 100644 index 0000000..ecb84a3 --- /dev/null +++ b/src/app/dashboard/invoices/[id]/edit/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { useParams } from "next/navigation"; +import InvoiceForm from "~/components/forms/invoice-form"; + +export default function InvoiceFormPage() { + const params = useParams(); + const id = params.id as string; + + // Pass the actual id, let the form component handle the logic + return ; +} diff --git a/src/components/ui/legal-modal.tsx b/src/components/ui/legal-modal.tsx new file mode 100644 index 0000000..ef10519 --- /dev/null +++ b/src/components/ui/legal-modal.tsx @@ -0,0 +1,346 @@ +"use client"; + +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; +import { Button } from "~/components/ui/button"; +import { ScrollArea } from "~/components/ui/scroll-area"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { X } from "lucide-react"; + +interface LegalModalProps { + type: "terms" | "privacy"; + trigger: React.ReactNode; +} + +export function LegalModal({ type, trigger }: LegalModalProps) { + const [open, setOpen] = useState(false); + + const isTerms = type === "terms"; + const title = isTerms ? "Terms of Service" : "Privacy Policy"; + + const TermsContent = () => ( +
+ + + Agreement to Terms + + +

+ These Terms of Service ("Terms") govern your use of the + beenvoice platform and services (the "Service") operated + by beenvoice ("us", "we", or "our"). +

+

+ By accessing or using our Service, you agree to be bound by these + Terms. If you disagree with any part of these terms, then you may + not access the Service. +

+
+
+ + + + Description of Service + + +

+ beenvoice is a web-based invoicing platform that allows users to: +

+
    +
  • Create and manage professional invoices
  • +
  • Track client information and billing details
  • +
  • Monitor payment status and financial metrics
  • +
  • Generate reports and analytics
  • +
  • Manage business profiles and settings
  • +
+
+
+ + + + User Accounts + + +

+ When you create an account with us, you must provide information + that is accurate, complete, and current at all times. You are + responsible for safeguarding the password and for all activities + that occur under your account. +

+

+ You agree not to disclose your password to any third party. You must + notify us immediately upon becoming aware of any breach of security + or unauthorized use of your account. +

+
+
+ + + + Acceptable Use + + +

You agree not to use the Service:

+
    +
  • + For any unlawful purpose or to solicit others to perform unlawful + acts +
  • +
  • + To violate any international, federal, provincial, or state + regulations, rules, laws, or local ordinances +
  • +
  • + To infringe upon or violate our intellectual property rights or + the intellectual property rights of others +
  • +
  • + To harass, abuse, insult, harm, defame, slander, disparage, + intimidate, or discriminate +
  • +
  • To submit false or misleading information
  • +
  • + To upload or transmit viruses or any other type of malicious code +
  • +
  • To spam, phish, pharm, pretext, spider, crawl, or scrape
  • +
  • For any obscene or immoral purpose
  • +
  • + To interfere with or circumvent the security features of the + Service +
  • +
+
+
+ + + + Payment Terms + + +

+ Some aspects of the Service may require payment. You will be charged + according to your subscription plan. All fees are non-refundable + unless otherwise stated. +

+

+ We may change our fees at any time. We will provide you with + reasonable notice of any fee changes by posting the new fees on the + Service or sending you email notification. +

+
+
+ + + + Termination + + +

+ We may terminate or suspend your account and bar access to the + Service immediately, without prior notice or liability, under our + sole discretion, for any reason whatsoever and without limitation, + including but not limited to a breach of the Terms. +

+

+ If you wish to terminate your account, you may simply discontinue + using the Service and contact us to request account deletion. +

+
+
+ + + + Contact Information + + +

+ If you have any questions about these Terms of Service, please + contact us at: +

+
    +
  • Email: legal@beenvoice.com
  • +
+
+
+
+ ); + + const PrivacyContent = () => ( +
+ + + Information We Collect + + +

Personal Information

+

+ We may collect personal information that you voluntarily provide to + us when you: +

+
    +
  • Register for an account
  • +
  • Create invoices or manage client information
  • +
  • Contact us for support
  • +
+

This personal information may include:

+
    +
  • Name and contact information (email, phone, address)
  • +
  • Business information and tax details
  • +
  • Client information you input into the system
  • +
  • Financial information related to your invoices
  • +
+
+
+ + + + How We Use Your Information + + +

We use the information we collect to:

+
    +
  • Provide, operate, and maintain our Service
  • +
  • Process your transactions and manage your account
  • +
  • Improve and personalize your experience
  • +
  • Communicate with you about your account and our services
  • +
  • Send you technical notices and support messages
  • +
  • Respond to your comments, questions, and requests
  • +
  • Monitor usage and analyze trends
  • +
  • + Detect, prevent, and address technical issues and security + breaches +
  • +
+
+
+ + + + How We Share Your Information + + +

+ We do not sell, trade, or rent your personal information to third + parties. We may share your information in the following + circumstances: +

+ +

Service Providers

+

+ We may share your information with trusted third-party service + providers who assist us in operating our Service, such as: +

+
    +
  • Cloud hosting and storage providers
  • +
  • Payment processors
  • +
  • Email service providers
  • +
  • Analytics and monitoring services
  • +
+ +

Legal Requirements

+

+ We may disclose your information if required to do so by law or in + response to: +

+
    +
  • Legal processes (subpoenas, court orders)
  • +
  • Government requests
  • +
  • Law enforcement investigations
  • +
  • Protection of our rights, property, or safety
  • +
+
+
+ + + + Data Security + + +

+ We implement appropriate technical and organizational security + measures to protect your information: +

+
    +
  • Encryption of data in transit and at rest
  • +
  • Secure access controls and authentication
  • +
  • Regular security assessments and updates
  • +
  • Employee training on data protection
  • +
  • Incident response procedures
  • +
+
+
+ + + + Your Rights and Choices + + +

+ Depending on your location, you may have the following rights + regarding your personal information: +

+
    +
  • Request access to your personal information
  • +
  • Correct inaccurate or incomplete information
  • +
  • Request deletion of your personal information
  • +
  • Restrict the processing of your information
  • +
  • Object to certain uses of your data
  • +
+

+ To exercise these rights, please contact us at + privacy@beenvoice.com. +

+
+
+ + + + Contact Us + + +

+ If you have questions about this Privacy Policy or our privacy + practices, please contact us at: +

+
    +
  • Email: privacy@beenvoice.com
  • +
+
+
+
+ ); + + return ( + + setOpen(true)}> + {trigger} + + + + + {title} + + + + + {isTerms ? : } + +
+ +
+
+
+ ); +} diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..f7b6d65 --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,26 @@ +"use client"; + +import * as React from "react"; +import { cn } from "~/lib/utils"; + +interface ScrollAreaProps extends React.HTMLAttributes { + children: React.ReactNode; +} + +const ScrollArea = React.forwardRef( + ({ className, children, ...props }, ref) => ( +
+ {children} +
+ ), +); +ScrollArea.displayName = "ScrollArea"; + +export { ScrollArea }; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..2b0849a --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { Toaster as Sonner, type ToasterProps } from "sonner"; + +const Toaster = ({ ...props }: ToasterProps) => { + return ( + + ); +}; + +export { Toaster }; diff --git a/src/lib/email-templates/password-reset-email.ts b/src/lib/email-templates/password-reset-email.ts new file mode 100644 index 0000000..d9b25d9 --- /dev/null +++ b/src/lib/email-templates/password-reset-email.ts @@ -0,0 +1,232 @@ +import { formatEmailDate } from "src/lib/email-utils"; + +interface PasswordResetEmailProps { + userEmail: string; + userName?: string; + resetToken: string; + resetUrl: string; + expiryHours?: number; +} + +export function generatePasswordResetEmailTemplate({ + userEmail, + userName, + resetUrl, + expiryHours = 24, +}: PasswordResetEmailProps) { + const displayName = userName ?? userEmail.split("@")[0]; + const currentDate = formatEmailDate(new Date()); + + // HTML version + const html = ` + + + + + + Password Reset - beenvoice + + + +
+
+

+ $ beenvoice +

+
+ +
+

Reset Your Password

+ +

Hello ${displayName},

+ +

+ We received a request to reset the password for your beenvoice account. + If you made this request, click the button below to set a new password. +

+ + + +

+ If the button doesn't work, copy and paste this link into your browser: +
+ ${resetUrl} +

+ +
+

Security Information

+

This password reset link will expire in ${expiryHours} hours for your security.

+

If you didn't request this password reset, you can safely ignore this email.

+
+ +
+ +

+ If you're having trouble accessing your account or have questions, + please contact our support team. +

+ +

+ Best regards,
+ The beenvoice Team +

+
+ + +
+ +`; + + // Plain text version + const text = ` +beenvoice - Password Reset + +Hello ${displayName}, + +We received a request to reset the password for your beenvoice account. + +To reset your password, please visit this link: +${resetUrl} + +SECURITY INFORMATION: +- This link will expire in ${expiryHours} hours +- If you didn't request this reset, you can safely ignore this email +- Never share this link with anyone + +If you're having trouble with the link, copy and paste the entire URL into your browser's address bar. + +If you have any questions or need assistance, please contact our support team at support@beenvoice.com. + +Best regards, +The beenvoice Team + +--- +This email was sent to ${userEmail} on ${currentDate}. +beenvoice - Professional invoicing made simple +`; + + return { + html: html.trim(), + text: text.trim(), + subject: "Reset Your beenvoice Password", + }; +}