{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "social-auth-react", "title": "Social Authentication (React)", "description": "Social authentication (OAuth) components for React with Convex Auth. Supports GitHub and Google sign-in with full Convex backend.", "dependencies": [ "lucide-react", "@convex-dev/auth@latest", "convex@latest", "@auth/core" ], "registryDependencies": ["button", "card", "alert"], "files": [ { "path": "src/registry/convex/blocks/social-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 CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { useAuthActions } from \"@convex-dev/auth/react\";\nimport { Loader2 } from \"lucide-react\";\nimport { useState } from \"react\";\n\nfunction GitHubIcon({ className }: { className?: string }) {\n return (\n \n \n \n );\n}\n\nfunction GoogleIcon({ className }: { className?: string }) {\n return (\n \n \n \n \n \n \n );\n}\n\ntype Provider = \"github\" | \"google\";\n\ninterface SocialLoginFormProps {\n providers?: Provider[];\n}\n\nexport function SocialLoginForm({\n providers = [\"github\", \"google\"],\n}: SocialLoginFormProps) {\n const { signIn } = useAuthActions();\n const [error, setError] = useState(null);\n const [loadingProvider, setLoadingProvider] = useState(null);\n\n const handleSocialLogin = async (provider: Provider) => {\n setError(null);\n setLoadingProvider(provider);\n\n try {\n await signIn(provider);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : `Failed to sign in with ${provider}`,\n );\n setLoadingProvider(null);\n }\n };\n\n const providerConfig = {\n github: {\n name: \"GitHub\",\n icon: GitHubIcon,\n },\n google: {\n name: \"Google\",\n icon: GoogleIcon,\n },\n };\n\n return (\n \n \n Welcome\n \n Sign in with your social account\n \n \n \n {error && (\n \n {error}\n \n )}\n \n {providers.map((provider) => {\n const config = providerConfig[provider];\n const Icon = config.icon;\n const isLoading = loadingProvider === provider;\n\n return (\n handleSocialLogin(provider)}\n disabled={loadingProvider !== null}\n >\n {isLoading ? (\n \n ) : (\n \n )}\n Continue with {config.name}\n \n );\n })}\n \n \n \n );\n}\n", "type": "registry:component", "target": "components/login-form.tsx" }, { "path": "src/registry/convex/blocks/social-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 * Extends all standard Button props for full customization.\n */\ninterface LogoutButtonProps extends React.ComponentProps {\n /** Whether to display the logout icon. Defaults to true. */\n showIcon?: boolean;\n /** Optional callback invoked after successful sign out. */\n onLogout?: () => void;\n}\n\n/**\n * A reusable logout button component for React applications using Convex Auth.\n * Handles the sign-out flow and provides optional callback support.\n *\n * @example\n * // Basic usage\n * \n *\n * @example\n * // With callback and custom styling\n * navigate('/login')}\n * variant=\"destructive\"\n * showIcon={false}\n * >\n * Log out\n * \n */\nexport function LogoutButton({\n showIcon = true,\n onLogout,\n children,\n ...props\n}: LogoutButtonProps) {\n const { signOut } = useAuthActions();\n\n /**\n * Handles the sign-out process.\n * Signs the user out via Convex Auth, then invokes the optional callback.\n */\n const handleLogout = async () => {\n await signOut();\n onLogout?.();\n };\n\n return (\n \n {showIcon && }\n {children ?? \"Sign out\"}\n \n );\n}\n", "type": "registry:component", "target": "components/logout-button.tsx" }, { "path": "src/registry/convex/blocks/social-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 social (OAuth) 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 is set when OAuth provider verifies email\n * - Profile data (name, image) is populated from OAuth provider\n */\nexport default defineSchema({\n // Auth tables from @convex-dev/auth (authAccounts, authSessions, etc.)\n ...authTables,\n\n // Users table with profile information from OAuth providers\n users: defineTable({\n // Profile fields (populated from OAuth provider)\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/social-auth-react/convex/auth.ts", "content": "import { convexAuth } from \"@convex-dev/auth/server\";\nimport GitHub from \"@auth/core/providers/github\";\nimport Google from \"@auth/core/providers/google\";\n\n// Build providers list dynamically based on available env vars\nconst providers: Parameters[0][\"providers\"] = [];\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// Fallback: if no providers configured, add both (they will fail gracefully at runtime)\nif (providers.length === 0) {\n providers.push(GitHub, Google);\n}\n\n/**\n * Convex Auth configuration for social (OAuth) authentication.\n *\n * Features:\n * - GitHub OAuth (if AUTH_GITHUB_ID/SECRET configured)\n * - Google OAuth (if AUTH_GOOGLE_ID/SECRET configured)\n * - OAuth 2.0 with PKCE\n * - Automatic email verification from trusted providers\n * - Secure session management\n *\n * Environment variables:\n * - AUTH_GITHUB_ID, AUTH_GITHUB_SECRET: GitHub OAuth credentials\n * - AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET: Google OAuth credentials\n *\n * Callback URLs (configure in provider dashboard):\n * - GitHub: https://YOUR_CONVEX_URL/api/auth/callback/github\n * - Google: https://YOUR_CONVEX_URL/api/auth/callback/google\n */\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers,\n callbacks: {\n /**\n * Called when a user signs in via OAuth.\n * Creates the user record if it doesn't exist, or updates profile data.\n */\n async createOrUpdateUser(ctx, args) {\n if (args.existingUserId) {\n // Existing user - update profile from OAuth provider if needed\n const user = await ctx.db.get(args.existingUserId);\n if (user && args.profile) {\n const updates: {\n name?: string;\n image?: string;\n emailVerificationTime?: number;\n } = {};\n\n // Update profile fields if provider has newer data\n if (args.profile.name && args.profile.name !== user.name) {\n updates.name = args.profile.name;\n }\n if (args.profile.image && args.profile.image !== user.image) {\n updates.image = args.profile.image;\n }\n // Mark email as verified if provider verifies it\n if (args.profile.emailVerified && !user.emailVerificationTime) {\n updates.emailVerificationTime = Date.now();\n }\n\n if (Object.keys(updates).length > 0) {\n await ctx.db.patch(args.existingUserId, updates);\n }\n }\n return args.existingUserId;\n }\n\n // Create new user with profile from OAuth provider\n const userId = await ctx.db.insert(\"users\", {\n name: args.profile?.name,\n email: args.profile?.email,\n image: args.profile?.image,\n // OAuth providers that verify email set emailVerified to true\n emailVerificationTime: args.profile?.emailVerified\n ? Date.now()\n : undefined,\n isAnonymous: false,\n });\n\n return userId;\n },\n },\n});\n", "type": "registry:file", "target": "convex/auth.ts" }, { "path": "src/registry/convex/blocks/social-auth-react/convex/auth.config.ts", "content": "import { AuthConfig } from \"convex/server\";\n\n/**\n * Auth configuration for Convex Auth with 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_GITHUB_ID, AUTH_GITHUB_SECRET: GitHub OAuth credentials\n * - AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET: Google OAuth credentials\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/social-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/social-auth-react/convex/http.ts", "content": "import { httpRouter } from \"convex/server\";\nimport { auth } from \"./auth\";\n\n/**\n * HTTP router for Convex Auth with OAuth providers.\n *\n * This sets up the required HTTP endpoints for authentication:\n * - GET /api/auth/signin/github - Initiate GitHub OAuth\n * - GET /api/auth/signin/google - Initiate Google OAuth\n * - GET /api/auth/callback/github - GitHub OAuth callback\n * - GET /api/auth/callback/google - Google OAuth callback\n * - POST /api/auth/signout - Sign out\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_GITHUB_ID": "", "AUTH_GITHUB_SECRET": "", "AUTH_GOOGLE_ID": "", "AUTH_GOOGLE_SECRET": "" }, "docs": "## Post-Install Setup\n\n1. Run `npx convex dev` to start the Convex dev server and get your deployment URL.\n\n2. Set environment variables in the Convex dashboard (Settings > Environment Variables):\n - `AUTH_GITHUB_ID` / `AUTH_GITHUB_SECRET` - GitHub OAuth credentials\n - `AUTH_GOOGLE_ID` / `AUTH_GOOGLE_SECRET` - Google OAuth credentials\n\n3. Configure OAuth callback URLs in each provider's dashboard:\n - GitHub: `https:///api/auth/callback/github`\n - Google: `https:///api/auth/callback/google`\n\nAt least one OAuth provider must be configured for authentication to work.\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" }