Skip to content

Commit 804e1b9

Browse files
nipunn1313erquhart
authored and
Convex, Inc.
committed
convex-js-convex-react-query PR 14: Add auth example (#37185)
Adds Convex Auth to example app w/ unverified email/password for simplicity. Co-authored-by: Shawn Erquhart <shawn@erquh.art> Co-authored-by: Shawn Erquhart <shawn@erquh.art> GitOrigin-RevId: 689d0d3ad15eabf6e6bbed12362ab1640b89983e
1 parent 850af2e commit 804e1b9

File tree

15 files changed

+731
-42
lines changed

15 files changed

+731
-42
lines changed

npm-packages/@convex-dev/react-query/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,10 @@ const { mutate } = useMutation({
9696
});
9797
```
9898

99-
# Authentication (example TODO)
99+
# Authentication
100+
101+
**Note:** The example app includes a basic Convex Auth implementation for
102+
reference.
100103

101104
TanStack Query isn't opionated about auth; an auth code might be a an element of
102105
a query key like any other. With Convex it's not necessary to add an additional

npm-packages/@convex-dev/react-query/convex/_generated/api.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import type {
1313
FilterApi,
1414
FunctionReference,
1515
} from "convex/server";
16+
import type * as auth from "../auth.js";
17+
import type * as http from "../http.js";
1618
import type * as messages from "../messages.js";
19+
import type * as user from "../user.js";
1720
import type * as weather from "../weather.js";
1821

1922
/**
@@ -25,7 +28,10 @@ import type * as weather from "../weather.js";
2528
* ```
2629
*/
2730
declare const fullApi: ApiFromModules<{
31+
auth: typeof auth;
32+
http: typeof http;
2833
messages: typeof messages;
34+
user: typeof user;
2935
weather: typeof weather;
3036
}>;
3137
export declare const api: FilterApi<
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default {
2+
providers: [
3+
{
4+
domain: process.env.CONVEX_SITE_URL,
5+
applicationID: "convex",
6+
},
7+
],
8+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Password } from "@convex-dev/auth/providers/Password";
2+
import { convexAuth } from "@convex-dev/auth/server";
3+
4+
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
5+
providers: [Password],
6+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { httpRouter } from "convex/server";
2+
import { auth } from "./auth";
3+
4+
const http = httpRouter();
5+
6+
auth.addHttpRoutes(http);
7+
8+
export default http;
Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,46 @@
11
import { mutation } from "./_generated/server.js";
22
import { query } from "./_generated/server.js";
3-
import { Doc } from "./_generated/dataModel.js";
3+
import { Doc, Id } from "./_generated/dataModel.js";
4+
import { v } from "convex/values";
5+
import schema, { vv } from "./schema.js";
46

57
export const list = query({
6-
handler: async (ctx): Promise<Doc<"messages">[]> => {
7-
return await ctx.db.query("messages").collect();
8+
returns: v.array(
9+
v.object({
10+
...vv.doc("messages").fields,
11+
authorId: v.id("users"),
12+
authorEmail: v.optional(v.string()),
13+
}),
14+
),
15+
handler: async (ctx) => {
16+
const messages = await ctx.db.query("messages").collect();
17+
return Promise.all(
18+
messages.map(async (message) => {
19+
const author = await ctx.db.get(message.author);
20+
if (!author) {
21+
throw new Error("Author not found");
22+
}
23+
return { ...message, authorId: author._id, authorEmail: author.email };
24+
}),
25+
);
826
},
927
});
1028

1129
export const count = query({
12-
handler: async (ctx): Promise<string> => {
30+
returns: v.string(),
31+
handler: async (ctx) => {
1332
const messages = await ctx.db.query("messages").take(1001);
1433
return messages.length === 1001 ? "1000+" : `${messages.length}`;
1534
},
1635
});
1736

1837
export const send = mutation({
19-
handler: async (ctx, { body, author }: { body: string; author: string }) => {
20-
const message = { body, author };
38+
args: {
39+
body: v.string(),
40+
author: v.id("users"),
41+
},
42+
handler: async (ctx, args) => {
43+
const message = { body: args.body, author: args.author };
2144
await ctx.db.insert("messages", message);
2245
},
2346
});
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import { defineSchema, defineTable } from "convex/server";
2+
import { authTables } from "@convex-dev/auth/server";
23
import { v } from "convex/values";
4+
import { typedV } from "convex-helpers/validators";
35

4-
export default defineSchema({
6+
const schema = defineSchema({
7+
...authTables,
58
messages: defineTable({
6-
author: v.string(),
9+
author: v.id("users"),
710
body: v.string(),
811
}),
912
});
13+
export default schema;
14+
15+
export const vv = typedV(schema);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { getAuthUserId } from "@convex-dev/auth/server";
2+
import { Doc } from "./_generated/dataModel";
3+
import { query } from "./_generated/server";
4+
5+
export const getCurrent = query({
6+
handler: async (ctx): Promise<Doc<"users"> | null> => {
7+
const userId = await getAuthUserId(ctx);
8+
if (!userId) {
9+
throw new Error("Unauthorized");
10+
}
11+
return await ctx.db.get(userId);
12+
},
13+
});

npm-packages/@convex-dev/react-query/package.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,8 @@
3838
"convex": "^1.24.1"
3939
},
4040
"devDependencies": {
41+
"@tanstack/eslint-plugin-query": "^5.74.7",
4142
"@tanstack/react-query": "^5.62.0",
42-
"convex": "workspace:*",
43-
"@tanstack/eslint-plugin-query": "^5.59.7",
4443
"@tanstack/react-query-devtools": "^5.62.0",
4544
"@types/node": "^18.17.0",
4645
"@types/react": "^18.0.0",
@@ -72,5 +71,13 @@
7271
},
7372
"module": "./dist/esm/index.js",
7473
"main": "./dist/commonjs/index.js",
75-
"types": "./dist/commonjs/index.d.ts"
74+
"types": "./dist/commonjs/index.d.ts",
75+
"dependencies": {
76+
"@auth/core": "^0.37.0",
77+
"@convex-dev/auth": "^0.0.83",
78+
"convex-helpers": "^0.1.85"
79+
},
80+
"overrides": {
81+
"typescript": "~5.0.3"
82+
}
7683
}

npm-packages/@convex-dev/react-query/src/example.tsx

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import {
77
useSuspenseQuery,
88
} from "@tanstack/react-query";
99
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
10-
import { ConvexProvider, ConvexReactClient } from "convex/react";
10+
import {
11+
Authenticated,
12+
AuthLoading,
13+
ConvexProvider,
14+
ConvexReactClient,
15+
Unauthenticated,
16+
} from "convex/react";
1117
import ReactDOM from "react-dom/client";
1218
import {
1319
ConvexQueryClient,
@@ -18,6 +24,7 @@ import {
1824
import "./index.css";
1925
import { FormEvent, useState } from "react";
2026
import { api } from "../convex/_generated/api.js";
27+
import { ConvexAuthProvider, useAuthActions } from "@convex-dev/auth/react";
2128

2229
// Build a global convexClient wherever you would normally create a TanStack Query client.
2330
const convexClient = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
@@ -38,12 +45,63 @@ convexQueryClient.connect(queryClient);
3845

3946
function Main() {
4047
return (
41-
<ConvexProvider client={convexClient}>
48+
<ConvexAuthProvider client={convexClient}>
4249
<QueryClientProvider client={queryClient}>
43-
<App />
50+
<AuthLoading>
51+
<div>Loading...</div>
52+
</AuthLoading>
53+
<Unauthenticated>
54+
<SignIn />
55+
</Unauthenticated>
56+
<Authenticated>
57+
<App />
58+
</Authenticated>
4459
<ReactQueryDevtools initialIsOpen />
4560
</QueryClientProvider>
46-
</ConvexProvider>
61+
</ConvexAuthProvider>
62+
);
63+
}
64+
65+
function SignIn() {
66+
const { signIn } = useAuthActions();
67+
const [step, setStep] = useState<"signUp" | "signIn">("signIn");
68+
return (
69+
<div className="signin-container">
70+
<form
71+
className="signin-form"
72+
onSubmit={(event) => {
73+
event.preventDefault();
74+
const formData = new FormData(event.currentTarget);
75+
void signIn("password", formData);
76+
}}
77+
>
78+
<input
79+
name="email"
80+
placeholder="Email"
81+
type="text"
82+
className="signin-input"
83+
/>
84+
<input
85+
name="password"
86+
placeholder="Password"
87+
type="password"
88+
className="signin-input"
89+
/>
90+
<input name="flow" type="hidden" value={step} />
91+
<button type="submit">
92+
{step === "signIn" ? "Sign in" : "Sign up"}
93+
</button>
94+
<button
95+
type="button"
96+
className="signin-secondary"
97+
onClick={() => {
98+
setStep(step === "signIn" ? "signUp" : "signIn");
99+
}}
100+
>
101+
{step === "signIn" ? "Sign up instead" : "Sign in instead"}
102+
</button>
103+
</form>
104+
</div>
47105
);
48106
}
49107

@@ -90,22 +148,30 @@ function MessageCount() {
90148
}
91149

92150
function App() {
151+
const { signOut } = useAuthActions();
93152
const { data, error, isPending } = useQuery({
94153
// This query updates reactively.
95154
...convexQuery(api.messages.list, {}),
96155
initialData: [],
97156
});
157+
const {
158+
data: user,
159+
error: userError,
160+
isPending: userIsPending,
161+
} = useQuery({
162+
...convexQuery(api.user.getCurrent, {}),
163+
initialData: null,
164+
});
98165

99166
const [newMessageText, setNewMessageText] = useState("");
100167
const { mutate, isPending: sending } = useMutation({
101168
mutationFn: useConvexMutation(api.messages.send),
102169
});
103-
const [name] = useState(() => "User " + Math.floor(Math.random() * 10000));
104170
async function handleSendMessage(event: FormEvent) {
105171
event.preventDefault();
106172
if (!sending && newMessageText) {
107173
mutate(
108-
{ body: newMessageText, author: name },
174+
{ body: newMessageText, author: user?._id },
109175
{
110176
onSuccess: () => setNewMessageText(""),
111177
},
@@ -120,16 +186,19 @@ function App() {
120186
}
121187
return (
122188
<main>
189+
<button type="button" onClick={() => void signOut()}>
190+
Sign out
191+
</button>
123192
<h1>Convex Chat</h1>
124193
<Weather />
125194
<MessageCount />
126195
<p className="badge">
127-
<span>{name}</span>
196+
<span>{user?.email}</span>
128197
</p>
129198
<ul>
130199
{data.map((message) => (
131200
<li key={message._id}>
132-
<span>{message.author}:</span>
201+
<span>{message.authorEmail}:</span>
133202
<span>{message.body}</span>
134203
<span>{new Date(message._creationTime).toLocaleTimeString()}</span>
135204
</li>

npm-packages/@convex-dev/react-query/src/index.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,37 @@ input[type="submit"]:disabled,
123123
button:disabled {
124124
background-color: rgb(122, 160, 248);
125125
}
126+
127+
.signin-container {
128+
display: flex;
129+
justify-content: center;
130+
margin-top: 32px;
131+
}
132+
133+
.signin-form {
134+
display: flex;
135+
flex-direction: column;
136+
gap: 8px;
137+
align-items: center;
138+
}
139+
140+
.signin-input {
141+
border: 1px solid #ced4da;
142+
border-radius: 8px;
143+
padding: 6px 12px;
144+
font-size: 16px;
145+
color: rgb(33, 37, 41);
146+
background: white;
147+
}
148+
149+
.signin-secondary {
150+
background: none;
151+
color: #316cf4;
152+
box-shadow: none;
153+
border: none;
154+
padding: 0;
155+
font-size: 16px;
156+
margin-left: 0;
157+
margin-top: 4px;
158+
cursor: pointer;
159+
}

0 commit comments

Comments
 (0)