diff --git a/README.md b/README.md index 0b8c20c..af0a7c4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Welcome to your Convex + React (Vite) app +# Welcome to your Convex + React (Vite) + Convex Auth app This is a [Convex](https://convex.dev/) project created with [`npm create convex`](https://www.npmjs.com/package/create-convex). @@ -7,7 +7,8 @@ After the initial setup (<2 minutes) you'll have a working full-stack app using: - Convex as your backend (database, server logic) - [React](https://react.dev/) as your frontend (web page interactivity) - [Vite](https://vitest.dev/) for optimized web hosting -- [Tailwind](https://tailwindcss.com/) for building great looking accessible UI +- [Tailwind](https://tailwindcss.com/) for building great looking UI +- [Convex Auth](https://labs.convex.dev/auth) for authentication ## Get started @@ -21,9 +22,13 @@ npm run dev If you're reading this README on GitHub and want to use this template, run: ``` -npm create convex@latest -- -t react-vite +npm create convex@latest -- -t react-vite-convexauth ``` +For more information on how to configure Convex Auth, check out the [Convex Auth docs](https://labs.convex.dev/auth/). + +For more examples of different Convex Auth flows, check out this [example repo](https://www.convex.dev/templates/convex-auth). + ## Learn more To learn more about developing your project with Convex, check out: diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 6a067d5..09899f7 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -13,6 +13,8 @@ import type { FilterApi, FunctionReference, } from "convex/server"; +import type * as auth from "../auth.js"; +import type * as http from "../http.js"; import type * as myFunctions from "../myFunctions.js"; /** @@ -24,6 +26,8 @@ import type * as myFunctions from "../myFunctions.js"; * ``` */ declare const fullApi: ApiFromModules<{ + auth: typeof auth; + http: typeof http; myFunctions: typeof myFunctions; }>; export declare const api: FilterApi< diff --git a/convex/auth.ts b/convex/auth.ts index cdfe776..e292e03 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -1,5 +1,6 @@ import { convexAuth } from "@convex-dev/auth/server"; +import { Password } from "@convex-dev/auth/providers/Password"; export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ - providers: [], + providers: [Password], }); diff --git a/convex/myFunctions.ts b/convex/myFunctions.ts index 0896dc3..a1b7a04 100644 --- a/convex/myFunctions.ts +++ b/convex/myFunctions.ts @@ -1,6 +1,7 @@ import { v } from "convex/values"; import { query, mutation, action } from "./_generated/server"; import { api } from "./_generated/api"; +import { getAuthUserId } from "@convex-dev/auth/server"; // Write your Convex functions in any file inside this directory (`convex`). // See https://docs.convex.dev/functions for more. @@ -21,8 +22,10 @@ export const listNumbers = query({ // Ordered by _creationTime, return most recent .order("desc") .take(args.count); + const userId = await getAuthUserId(ctx); + const user = userId === null ? null : await ctx.db.get(userId); return { - viewer: (await ctx.auth.getUserIdentity())?.name ?? null, + viewer: user?.email ?? null, numbers: numbers.reverse().map((number) => number.value), }; }, diff --git a/convex/schema.ts b/convex/schema.ts index b1bc23e..cae4774 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -1,11 +1,12 @@ import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; +import { authTables } from "@convex-dev/auth/server"; -// The schema is entirely optional. -// You can delete this file (schema.ts) and the -// app will continue to work. +// The schema is normally optional, but Convex Auth +// requires indexes defined on `authTables`. // The schema provides more precise TypeScript types. export default defineSchema({ + ...authTables, numbers: defineTable({ value: v.number(), }), diff --git a/package.json b/package.json index 5c8df57..0c838a5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "npm-run-all --parallel dev:frontend dev:backend", "dev:frontend": "vite --open", "dev:backend": "convex dev", - "predev": "convex dev --until-success && convex dashboard", + "predev": "convex dev --until-success && convex dev --once --run-sh \"node setup.mjs --once\" && convex dashboard", "build": "tsc -b && vite build", "lint": "tsc && eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" @@ -25,6 +25,7 @@ "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", + "dotenv": "^16.4.7", "eslint": "^9.21.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", diff --git a/setup.mjs b/setup.mjs new file mode 100644 index 0000000..0ffb1ae --- /dev/null +++ b/setup.mjs @@ -0,0 +1,35 @@ +/** + * This script runs `npx @convex-dev/auth` to help with setting up + * environment variables for Convex Auth. + * + * You can safely delete it and remove it from package.json scripts. + */ + +import fs from "fs"; +import { config as loadEnvFile } from "dotenv"; +import { spawnSync } from "child_process"; + +if (!fs.existsSync(".env.local")) { + // Something is off, skip the script. + process.exit(0); +} + +const config = {}; +loadEnvFile({ path: ".env.local", processEnv: config }); + +const runOnceWorkflow = process.argv.includes("--once"); + +if (runOnceWorkflow && config.SETUP_SCRIPT_RAN !== undefined) { + // The script has already ran once, skip. + process.exit(0); +} + +const result = spawnSync("npx", ["@convex-dev/auth", "--skip-git-check"], { + stdio: "inherit", +}); + +if (runOnceWorkflow) { + fs.writeFileSync(".env.local", `\nSETUP_SCRIPT_RAN=1\n`, { flag: "a" }); +} + +process.exit(result.status); diff --git a/src/App.tsx b/src/App.tsx index 588725b..2bdbec1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,116 @@ "use client"; -import { useMutation, useQuery } from "convex/react"; +import { + Authenticated, + Unauthenticated, + useConvexAuth, + useMutation, + useQuery, +} from "convex/react"; import { api } from "../convex/_generated/api"; +import { useAuthActions } from "@convex-dev/auth/react"; +import { useState } from "react"; export default function App() { return ( <>
- Convex + React + Convex + React + Convex Auth +
-

Convex + React

- +

+ Convex + React + Convex Auth +

+ + + + + +
); } +function SignOutButton() { + const { isAuthenticated } = useConvexAuth(); + const { signOut } = useAuthActions(); + return ( + <> + {isAuthenticated && ( + + )} + + ); +} + +function SignInForm() { + const { signIn } = useAuthActions(); + const [flow, setFlow] = useState<"signIn" | "signUp">("signIn"); + const [error, setError] = useState(null); + return ( +
+

Log in to see the numbers

+
{ + e.preventDefault(); + const formData = new FormData(e.target as HTMLFormElement); + formData.set("flow", flow); + void signIn("password", formData).catch((error) => { + setError(error.message); + }); + }} + > + + + +
+ + {flow === "signIn" + ? "Don't have an account?" + : "Already have an account?"} + + setFlow(flow === "signIn" ? "signUp" : "signIn")} + > + {flow === "signIn" ? "Sign up instead" : "Sign in instead"} + +
+ {error && ( +
+

+ Error signing in: {error} +

+
+ )} +
+
+ ); +} + function Content() { const { viewer, numbers } = useQuery(api.myFunctions.listNumbers, { diff --git a/src/main.tsx b/src/main.tsx index 4880906..5f5a1bd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,14 +1,16 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import { ConvexProvider, ConvexReactClient } from "convex/react"; +import { ConvexAuthProvider } from "@convex-dev/auth/react"; +import { ConvexReactClient } from "convex/react"; import "./index.css"; import App from "./App.tsx"; const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string); + createRoot(document.getElementById("root")!).render( - + - + , );