{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "password-based-auth-react", "title": "Password-Based Authentication (React)", "description": "Complete password-based authentication flow for React with Convex Auth. Includes login, sign-up, password reset forms, and full Convex backend.", "dependencies": [ "lucide-react", "@convex-dev/auth@latest", "convex@latest", "@auth/core" ], "registryDependencies": [ "button", "input", "label", "card", "alert", "avatar", "skeleton" ], "files": [ { "path": "src/registry/convex/blocks/password-based-auth-react/components/login-form.tsx", "content": "\"use client\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useState } from \"react\";\n\ninterface LoginFormProps {\n onSignUpClick?: () => void;\n onForgotPasswordClick?: () => void;\n}\n\nexport function LoginForm({\n onSignUpClick,\n onForgotPasswordClick,\n}: LoginFormProps) {\n const { signIn } = useAuthActions();\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [error, setError] = useState(null);\n const [loading, setLoading] = useState(false);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n setLoading(true);\n\n try {\n const formData = new FormData();\n formData.set(\"email\", email);\n formData.set(\"password\", password);\n formData.set(\"flow\", \"signIn\");\n\n await signIn(\"password\", formData);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to sign in\");\n } finally {\n setLoading(false);\n }\n };\n\n return (\n \n \n Welcome back\n Sign in to your account to continue\n \n
\n \n {error && (\n \n {error}\n \n )}\n
\n \n setEmail(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n
\n \n {onForgotPasswordClick && (\n \n Forgot password?\n \n )}\n
\n setPassword(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n \n {onSignUpClick && (\n <>\n
\n
\n \n
\n
\n or\n
\n
\n

\n Don't have an account?{\" \"}\n \n Sign up\n \n

\n \n )}\n
\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/login-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-react/components/sign-up-form.tsx", "content": "\"use client\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useState } from \"react\";\n\ninterface SignUpFormProps {\n onLoginClick?: () => void;\n onSuccess?: () => void;\n}\n\nexport function SignUpForm({ onLoginClick, onSuccess }: SignUpFormProps) {\n const { signIn } = useAuthActions();\n const [name, setName] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [confirmPassword, setConfirmPassword] = useState(\"\");\n const [error, setError] = useState(null);\n const [loading, setLoading] = useState(false);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (password !== confirmPassword) {\n setError(\"Passwords do not match\");\n return;\n }\n\n if (password.length < 8) {\n setError(\"Password must be at least 8 characters\");\n return;\n }\n\n setLoading(true);\n\n try {\n const formData = new FormData();\n formData.set(\"name\", name);\n formData.set(\"email\", email);\n formData.set(\"password\", password);\n formData.set(\"flow\", \"signUp\");\n\n await signIn(\"password\", formData);\n onSuccess?.();\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to create account\");\n } finally {\n setLoading(false);\n }\n };\n\n return (\n \n \n Create an account\n Enter your details to get started\n \n
\n \n {error && (\n \n {error}\n \n )}\n
\n \n setName(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n setEmail(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n setPassword(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n setConfirmPassword(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n \n {onLoginClick && (\n <>\n
\n
\n \n
\n
\n or\n
\n
\n

\n Already have an account?{\" \"}\n \n Sign in\n \n

\n \n )}\n
\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/sign-up-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-react/components/forgot-password-form.tsx", "content": "\"use client\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\nimport { ArrowLeft, CheckCircle, Loader2 } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { useState } from \"react\";\n\n/**\n * Props for the ForgotPasswordForm component.\n *\n * @property onBackToLoginClick - Callback when user clicks \"Back to login\"\n * @property onEnterCodeClick - Callback when user clicks \"Enter reset code\" after success.\n * Receives the email address to pass along to the update password form.\n */\ninterface ForgotPasswordFormProps {\n onBackToLoginClick?: () => void;\n onEnterCodeClick?: (email: string) => void;\n}\n\n/**\n * ForgotPasswordForm component for requesting password reset.\n *\n * Per Convex Auth docs, submits with flow: \"reset\" which triggers\n * the ResendOTPPasswordReset provider to send an OTP code via email.\n *\n * After successful submission, displays a success message and provides\n * callbacks for navigation (since this is framework-agnostic).\n */\nexport function ForgotPasswordForm({\n onBackToLoginClick,\n onEnterCodeClick,\n}: ForgotPasswordFormProps) {\n const { signIn } = useAuthActions();\n const [email, setEmail] = useState(\"\");\n const [error, setError] = useState(null);\n const [success, setSuccess] = useState(false);\n const [loading, setLoading] = useState(false);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n setLoading(true);\n\n try {\n const formData = new FormData();\n formData.set(\"email\", email);\n formData.set(\"flow\", \"reset\");\n\n await signIn(\"password\", formData);\n setSuccess(true);\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to send reset code\",\n );\n } finally {\n setLoading(false);\n }\n };\n\n // Success state - show confirmation and navigation options\n if (success) {\n return (\n \n \n
\n \n
\n Check your email\n \n We've sent a password reset code to {email}\n \n
\n \n Enter the 8-digit code from your email to reset your password.\n \n \n {onEnterCodeClick && (\n \n )}\n {onBackToLoginClick && (\n \n \n Back to login\n \n )}\n \n
\n );\n }\n\n return (\n \n \n Reset your password\n \n Enter your email and we'll send you a code to reset your password\n \n \n
\n \n {error && (\n \n {error}\n \n )}\n
\n \n setEmail(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n \n {loading && }\n Send reset code\n \n {onBackToLoginClick && (\n \n \n Back to login\n \n )}\n \n
\n
\n );\n}\n", "type": "registry:component", "target": "components/forgot-password-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-react/components/update-password-form.tsx", "content": "\"use client\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { ArrowLeft, Loader2 } from \"lucide-react\";\nimport { useState } from \"react\";\n\n/**\n * Props for the UpdatePasswordForm component.\n *\n * @property email - Pre-filled email address (e.g., passed from forgot password flow)\n * @property onSuccess - Callback when password is successfully updated\n * @property onRequestNewCodeClick - Callback when user clicks \"Request new code\"\n */\ninterface UpdatePasswordFormProps {\n email?: string;\n onSuccess?: () => void;\n onRequestNewCodeClick?: () => void;\n}\n\n/**\n * UpdatePasswordForm component for completing password reset.\n *\n * Supports two flows:\n * 1. OTP Code: User enters the 8-digit code received via email + new password\n * 2. Email prop: Email is passed as a prop from the forgot password flow\n *\n * Per Convex Auth docs, the form submits:\n * - code: The verification code\n * - email: The user's email address\n * - newPassword: The new password\n * - flow: \"reset-verification\"\n */\nexport function UpdatePasswordForm({\n email: emailProp,\n onSuccess,\n onRequestNewCodeClick,\n}: UpdatePasswordFormProps) {\n const { signIn } = useAuthActions();\n\n const [email, setEmail] = useState(emailProp ?? \"\");\n const [code, setCode] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [confirmPassword, setConfirmPassword] = useState(\"\");\n const [error, setError] = useState(null);\n const [loading, setLoading] = useState(false);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n // Validation\n if (!email) {\n setError(\"Email is required\");\n return;\n }\n\n if (!code) {\n setError(\"Verification code is required\");\n return;\n }\n\n if (password !== confirmPassword) {\n setError(\"Passwords do not match\");\n return;\n }\n\n if (password.length < 8) {\n setError(\"Password must be at least 8 characters\");\n return;\n }\n\n setLoading(true);\n\n try {\n const formData = new FormData();\n formData.set(\"code\", code);\n formData.set(\"email\", email);\n formData.set(\"newPassword\", password);\n formData.set(\"flow\", \"reset-verification\");\n\n await signIn(\"password\", formData);\n onSuccess?.();\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to update password\",\n );\n } finally {\n setLoading(false);\n }\n };\n\n return (\n \n \n Set new password\n \n Enter the code from your email and your new password\n \n \n
\n \n {error && (\n \n {error}\n \n )}\n\n {/* Only show email input if not provided via prop */}\n {!emailProp && (\n
\n \n setEmail(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n )}\n\n
\n \n setCode(e.target.value)}\n required\n disabled={loading}\n maxLength={8}\n pattern=\"[0-9]{8}\"\n className=\"h-11\"\n />\n
\n\n
\n \n setPassword(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n\n
\n \n setConfirmPassword(e.target.value)}\n required\n disabled={loading}\n className=\"h-11\"\n />\n
\n
\n \n \n {loading && }\n Update password\n \n {onRequestNewCodeClick && (\n \n \n Request new code\n \n )}\n \n
\n
\n );\n}\n", "type": "registry:component", "target": "components/update-password-form.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-react/components/logout-button.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { LogOut } from \"lucide-react\";\n\n/**\n * Props for the LogoutButton component.\n *\n * Extends all standard Button props with additional customization options.\n *\n * @property showIcon - Whether to show the logout icon (default: true)\n * @property onLogout - Optional callback invoked after successful logout\n */\ninterface LogoutButtonProps extends React.ComponentProps {\n showIcon?: boolean;\n onLogout?: () => void;\n}\n\n/**\n * LogoutButton component for signing out the current user.\n *\n * Uses Convex Auth's signOut action to clear the session.\n * Fully customizable through standard Button props.\n *\n * @example\n * // Basic usage\n * \n *\n * @example\n * // Custom styling and callback\n * navigate(\"/login\")}\n * >\n * Log out now\n * \n */\nexport function LogoutButton({\n showIcon = true,\n onLogout,\n children,\n ...props\n}: LogoutButtonProps) {\n const { signOut } = useAuthActions();\n\n const handleClick = async () => {\n await signOut();\n onLogout?.();\n };\n\n return (\n \n );\n}\n", "type": "registry:component", "target": "components/logout-button.tsx" }, { "path": "src/registry/convex/blocks/password-based-auth-react/convex/schema.ts", "content": "import { defineSchema, defineTable } from \"convex/server\";\nimport { authTables } from \"@convex-dev/auth/server\";\nimport { v } from \"convex/values\";\n\n/**\n * Schema for password-based authentication.\n * Includes the users table and all required auth tables from @convex-dev/auth.\n *\n * Security considerations:\n * - Email is indexed for fast lookups but never exposed in public queries\n * - emailVerificationTime tracks when email was verified\n * - isAnonymous flag supports gradual account upgrades\n */\nexport default defineSchema({\n // Auth tables from @convex-dev/auth (authAccounts, authSessions, authVerificationCodes, etc.)\n ...authTables,\n\n // Users table with profile information\n users: defineTable({\n // Profile fields\n name: v.optional(v.string()),\n email: v.optional(v.string()),\n image: v.optional(v.string()),\n\n // Auth metadata\n emailVerificationTime: v.optional(v.number()),\n isAnonymous: v.optional(v.boolean()),\n })\n .index(\"by_email\", [\"email\"])\n .index(\"by_anonymous\", [\"isAnonymous\"]),\n});\n", "type": "registry:file", "target": "convex/schema.ts" }, { "path": "src/registry/convex/blocks/password-based-auth-react/convex/auth.ts", "content": "import { convexAuth } from \"@convex-dev/auth/server\";\nimport { Password } from \"@convex-dev/auth/providers/Password\";\nimport Resend from \"@auth/core/providers/resend\";\nimport GitHub from \"@auth/core/providers/github\";\nimport Google from \"@auth/core/providers/google\";\nimport { DataModel } from \"./_generated/dataModel\";\n\n/**\n * Custom Resend provider for email verification OTP.\n *\n * Sends an 8-digit OTP code for email verification during sign-up.\n * Configure AUTH_RESEND_KEY environment variable to enable.\n */\nconst ResendOTP = Resend({\n id: \"resend-otp-verification\",\n apiKey: process.env.AUTH_RESEND_KEY,\n async generateVerificationToken() {\n const code = Array.from({ length: 8 }, () =>\n Math.floor(Math.random() * 10),\n ).join(\"\");\n return code;\n },\n async sendVerificationRequest({ identifier: email, provider, token }) {\n const response = await fetch(\"https://api.resend.com/emails\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${provider.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n from: process.env.AUTH_EMAIL_FROM ?? \"onboarding@resend.dev\",\n to: [email],\n subject: \"Verify your email\",\n text: `Your verification code is: ${token}\\n\\nThis code expires in 15 minutes.`,\n }),\n });\n\n if (!response.ok) {\n throw new Error(\"Could not send verification email\");\n }\n },\n});\n\n/**\n * Custom Resend provider for password reset OTP.\n *\n * Sends an 8-digit OTP code for password reset.\n * Configure AUTH_RESEND_KEY environment variable to enable.\n */\nconst ResendOTPPasswordReset = Resend({\n id: \"resend-otp-reset\",\n apiKey: process.env.AUTH_RESEND_KEY,\n async generateVerificationToken() {\n const code = Array.from({ length: 8 }, () =>\n Math.floor(Math.random() * 10),\n ).join(\"\");\n return code;\n },\n async sendVerificationRequest({ identifier: email, provider, token }) {\n const response = await fetch(\"https://api.resend.com/emails\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${provider.apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n from: process.env.AUTH_EMAIL_FROM ?? \"onboarding@resend.dev\",\n to: [email],\n subject: \"Reset your password\",\n text: `Your password reset code is: ${token}\\n\\nThis code expires in 15 minutes.`,\n }),\n });\n\n if (!response.ok) {\n throw new Error(\"Could not send password reset email\");\n }\n },\n});\n\n/**\n * Custom Password provider with:\n * - Email verification during sign-up (via ResendOTP)\n * - Password reset flow (via ResendOTPPasswordReset)\n * - Strong password requirements\n */\nconst CustomPassword = Password({\n // Require email verification during sign-up\n verify: ResendOTP,\n\n // Enable password reset via email\n reset: ResendOTPPasswordReset,\n\n // Custom password validation\n validatePasswordRequirements: (password: string) => {\n if (password.length < 8) {\n throw new Error(\"Password must be at least 8 characters\");\n }\n if (!/[a-z]/.test(password)) {\n throw new Error(\"Password must contain at least one lowercase letter\");\n }\n if (!/[A-Z]/.test(password)) {\n throw new Error(\"Password must contain at least one uppercase letter\");\n }\n if (!/[0-9]/.test(password)) {\n throw new Error(\"Password must contain at least one number\");\n }\n },\n\n // Customize profile to handle additional user fields\n profile(params) {\n return {\n email: params.email as string,\n name: (params.name as string) || undefined,\n };\n },\n});\n\n// Build providers list dynamically based on available env vars\nconst providers: Parameters[0][\"providers\"] = [\n CustomPassword,\n];\n\n// Only add GitHub if credentials are configured\nif (process.env.AUTH_GITHUB_ID && process.env.AUTH_GITHUB_SECRET) {\n providers.push(GitHub);\n}\n\n// Only add Google if credentials are configured\nif (process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET) {\n providers.push(Google);\n}\n\n/**\n * Convex Auth configuration.\n *\n * Features:\n * - Password authentication with email verification\n * - Password reset via OTP codes\n * - Strong password requirements\n * - Optional GitHub OAuth (if AUTH_GITHUB_ID/SECRET configured)\n * - Optional Google OAuth (if AUTH_GOOGLE_ID/SECRET configured)\n *\n * Environment variables:\n * - AUTH_RESEND_KEY: Resend API key for sending emails (required)\n * - AUTH_EMAIL_FROM: Sender email address (optional)\n * - AUTH_GITHUB_ID, AUTH_GITHUB_SECRET: GitHub OAuth (optional)\n * - AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET: Google OAuth (optional)\n */\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers,\n});\n", "type": "registry:file", "target": "convex/auth.ts" }, { "path": "src/registry/convex/blocks/password-based-auth-react/convex/auth.config.ts", "content": "import { AuthConfig } from \"convex/server\";\n\n/**\n * Auth configuration for Convex Auth with password + OAuth providers.\n *\n * The domain should match your Convex deployment URL.\n * This is automatically set via CONVEX_SITE_URL environment variable.\n *\n * Required environment variables:\n * - CONVEX_SITE_URL: Your Convex deployment URL\n * - AUTH_RESEND_KEY: Resend API key for email verification/reset\n *\n * Optional environment variables (for social login):\n * - AUTH_GITHUB_ID, AUTH_GITHUB_SECRET: GitHub OAuth credentials\n * - AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET: Google OAuth credentials\n * - AUTH_EMAIL_FROM: Sender email address (defaults to onboarding@resend.dev)\n *\n * Configure the OAuth callback URLs in your provider's settings:\n * - GitHub: https://YOUR_CONVEX_URL/api/auth/callback/github\n * - Google: https://YOUR_CONVEX_URL/api/auth/callback/google\n */\nexport default {\n providers: [\n {\n domain: process.env.CONVEX_SITE_URL!,\n applicationID: \"convex\",\n },\n ],\n} satisfies AuthConfig;\n", "type": "registry:file", "target": "convex/auth.config.ts" }, { "path": "src/registry/convex/blocks/password-based-auth-react/convex/users.ts", "content": "import { query, mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { getAuthUserId } from \"@convex-dev/auth/server\";\nimport { ConvexError } from \"convex/values\";\n\n/**\n * User validator for internal use.\n * Includes all fields - use publicProfileValidator for external queries.\n */\nconst userValidator = v.object({\n _id: v.id(\"users\"),\n _creationTime: v.number(),\n name: v.optional(v.string()),\n email: v.optional(v.string()),\n image: v.optional(v.string()),\n emailVerificationTime: v.optional(v.number()),\n isAnonymous: v.optional(v.boolean()),\n});\n\n/**\n * Public profile validator - excludes sensitive fields like email.\n * Use this when returning user data to other users.\n */\nconst publicProfileValidator = v.object({\n _id: v.id(\"users\"),\n name: v.optional(v.string()),\n image: v.optional(v.string()),\n});\n\n/**\n * Get the current authenticated user's full profile.\n * Returns null if not authenticated.\n *\n * Security: Only returns data for the authenticated user themselves.\n */\nexport const current = query({\n args: {},\n returns: v.union(userValidator, v.null()),\n handler: async (ctx: any) => {\n const userId = await getAuthUserId(ctx);\n if (!userId) {\n return null;\n }\n return await ctx.db.get(userId);\n },\n});\n\n/**\n * Get a user's public profile by ID.\n * Only returns safe, public fields (name, image).\n *\n * Security: Never exposes email or other sensitive data.\n */\nexport const get = query({\n args: { userId: v.id(\"users\") },\n returns: v.union(publicProfileValidator, v.null()),\n handler: async (ctx: any, args: any) => {\n const user = await ctx.db.get(args.userId);\n if (!user) {\n return null;\n }\n // Only return public fields\n return {\n _id: user._id,\n name: user.name,\n image: user.image,\n };\n },\n});\n\n/**\n * Update the current user's profile.\n * Only allows updating safe fields (name, image).\n *\n * Security:\n * - Requires authentication\n * - Only allows updating own profile\n * - Validates input types\n */\nexport const updateProfile = mutation({\n args: {\n name: v.optional(v.string()),\n image: v.optional(v.string()),\n },\n returns: v.null(),\n handler: async (ctx: any, args: any) => {\n const userId = await getAuthUserId(ctx);\n if (!userId) {\n throw new ConvexError({\n code: \"UNAUTHORIZED\",\n message: \"You must be logged in to update your profile\",\n });\n }\n\n // Build updates object with only provided fields\n const updates: any = {};\n if (args.name !== undefined) {\n // Sanitize name - trim whitespace, limit length\n const sanitizedName = args.name.trim().slice(0, 100);\n if (sanitizedName) {\n updates.name = sanitizedName;\n }\n }\n if (args.image !== undefined) {\n // Validate image URL format\n if (args.image && !isValidUrl(args.image)) {\n throw new ConvexError({\n code: \"INVALID_INPUT\",\n message: \"Invalid image URL\",\n });\n }\n updates.image = args.image;\n }\n\n if (Object.keys(updates).length > 0) {\n await ctx.db.patch(userId, updates);\n }\n\n return null;\n },\n});\n\n/**\n * Helper to validate URL format.\n */\nfunction isValidUrl(string: string): boolean {\n try {\n const url = new URL(string);\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n } catch {\n return false;\n }\n}\n", "type": "registry:file", "target": "convex/users.ts" }, { "path": "src/registry/convex/blocks/password-based-auth-react/convex/http.ts", "content": "import { httpRouter } from \"convex/server\";\nimport { auth } from \"./auth\";\n\n/**\n * HTTP router for Convex Auth.\n *\n * This sets up the required HTTP endpoints for authentication:\n * - POST /api/auth/signin - Sign in\n * - POST /api/auth/signout - Sign out\n * - GET /api/auth/signin/* - OAuth callbacks\n * - POST /api/auth/callback/* - OAuth callbacks\n *\n * These routes are automatically configured by auth.addHttpRoutes().\n */\nconst http = httpRouter();\n\n// Add all auth HTTP routes\nauth.addHttpRoutes(http);\n\nexport default http;\n", "type": "registry:file", "target": "convex/http.ts" }, { "path": "src/registry/convex/clients/react/lib/convex/client.ts", "content": "import { ConvexReactClient } from \"convex/react\";\n\nconst convexUrl = (import.meta as any).env.VITE_CONVEX_URL as string;\n\nexport const convex = new ConvexReactClient(convexUrl);\n", "type": "registry:lib", "target": "lib/convex/client.ts" }, { "path": "src/registry/convex/clients/react/lib/convex/provider.tsx", "content": "import { ConvexAuthProvider } from \"@convex-dev/auth/react\";\nimport { ConvexReactClient } from \"convex/react\";\nimport { ReactNode } from \"react\";\n\nconst convex = new ConvexReactClient(\n (import.meta as any).env.VITE_CONVEX_URL as string,\n);\n\nexport function ConvexClientProvider({ children }: { children: ReactNode }) {\n return {children};\n}\n", "type": "registry:lib", "target": "lib/convex/provider.tsx" } ], "envVars": { "CONVEX_DEPLOYMENT": "", "VITE_CONVEX_URL": "", "AUTH_RESEND_KEY": "", "AUTH_EMAIL_FROM": "" }, "docs": "## Post-Install Setup\n\n1. Run `npx convex dev` to start the Convex dev server and get your deployment URL.\n\n2. Copy the deployment URL to your `.env` file as `VITE_CONVEX_URL`.\n\n3. Set environment variables in the Convex dashboard (Settings > Environment Variables):\n - `AUTH_RESEND_KEY` - Your Resend API key (required for email verification/reset)\n - `AUTH_EMAIL_FROM` - Sender email address (optional, defaults to onboarding@resend.dev)\n\n4. Wrap your app with the `ConvexAuthProvider` from `lib/convex/provider.tsx`.\n\n## Post-Install Setup\n\n1. Run `npx convex dev` to start the Convex dev server and get your deployment URL.\n\n2. Copy the deployment URL to your `.env` file as `VITE_CONVEX_URL`.\n\n3. Wrap your app with the `ConvexAuthProvider` from `lib/convex/provider.tsx`.", "type": "registry:block" }