diff --git a/.env.example b/.env.example index 15ab490..ed492ce 100644 --- a/.env.example +++ b/.env.example @@ -20,10 +20,27 @@ TWILIO_ACCOUNT_SID= TWILIO_AUTH_TOKEN= TWILIO_PHONE_NUMBER= -# Stripe +# Stripe (client invoicing — client portal payments) STRIPE_SECRET_KEY= STRIPE_WEBHOOK_SECRET= +# Hosted SaaS subscriptions (managed service only — leave HOSTED_BILLING_ENABLED +# unset for self-host so the OSS edition runs fully unlocked). When enabled, +# plan tiers are enforced and these must be set. +HOSTED_BILLING_ENABLED= +# Cloud plan: $99/mo recurring Price, charged per location (Stripe quantity). +STRIPE_PRICE_CLOUD= +# Metered overage Prices for usage beyond the included monthly allowances. +STRIPE_PRICE_SMS_OVERAGE= +STRIPE_PRICE_AI_OVERAGE= +# Separate webhook endpoint secret for customer.subscription.* events. +STRIPE_SUBSCRIPTION_WEBHOOK_SECRET= +# Public base URL used to build Stripe success/return URLs (falls back to NEXTAUTH_URL). +NEXT_PUBLIC_APP_URL="http://localhost:3000" +# Comma-separated emails allowed into the cross-tenant platform admin dashboard +# (/admin). OpenVPM operators only — separate from a practice's own admin role. +PLATFORM_ADMIN_EMAILS= + # Cron job authentication CRON_SECRET= diff --git a/.gitignore b/.gitignore index 272fee7..229ea4f 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ docs/screenshots/audit/ test-results/ CAVSG_AI_Innovator_Survey_COMPLETED.docx package-lock.json + +# Local MCP server config (not for the public repo) +.mcp.json diff --git a/apps/web/app/(auth)/forgot-password/page.tsx b/apps/web/app/(auth)/forgot-password/page.tsx new file mode 100644 index 0000000..1f95da4 --- /dev/null +++ b/apps/web/app/(auth)/forgot-password/page.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { trpc } from "@/lib/trpc"; +import { toast } from "sonner"; + +export default function ForgotPasswordPage() { + const [email, setEmail] = useState(""); + const [sent, setSent] = useState(false); + + const request = trpc.auth.requestPasswordReset.useMutation({ + onSuccess: () => setSent(true), + onError: (err) => toast.error(err.message), + }); + + return ( +
Reset your password
++ If an account exists for {email}, we've sent a reset link. + Check your inbox. +
+ ) : ( + + )} + ++ + Back to sign in + +
++ + Forgot your password? + +
+Don't have an account?{" "} Register your practice diff --git a/apps/web/app/(auth)/register/page.tsx b/apps/web/app/(auth)/register/page.tsx index 7ec99f1..1406f78 100644 --- a/apps/web/app/(auth)/register/page.tsx +++ b/apps/web/app/(auth)/register/page.tsx @@ -16,16 +16,23 @@ export default function RegisterPage() { const [error, setError] = useState(""); const [loading, setLoading] = useState(false); + const [verifySent, setVerifySent] = useState(false); + const registerMutation = trpc.auth.register.useMutation({ - onSuccess: async () => { + onSuccess: async (data) => { + // Hosted: email verification required → show a "check your inbox" notice. + if (data?.verificationRequired) { + setVerifySent(true); + setLoading(false); + return; + } + // Self-host: auto-sign in after registration. toast.success("Account created! Redirecting..."); - // Auto-sign in after registration const result = await signIn("credentials", { email, password, redirect: false, }); - if (result?.ok) { router.push("/"); router.refresh(); @@ -38,6 +45,23 @@ export default function RegisterPage() { }, }); + if (verifySent) { + return ( +
+ We sent a verification link to {email}. Click it to activate + your account and start your 14-day free trial. +
+ + Back to sign in + +Register your practice
++ 14-day free trial · no credit card required +