-
Notifications
You must be signed in to change notification settings - Fork 498
convex example testing #943
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7741c72
6d0bab5
9e28cd2
c510f29
e3d9f18
18fd2ea
0974f71
8b1a5b3
b460774
e2c4b1a
d93fad8
3cf9875
07bfe17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,3 +39,5 @@ yarn-error.log* | |
| # typescript | ||
| *.tsbuildinfo | ||
| next-env.d.ts | ||
|
|
||
| .env.local | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| "use client"; | ||
|
|
||
| import { useAction } from "convex/react"; | ||
| import { useState } from "react"; | ||
| import { api } from "@/convex/_generated/api"; | ||
| import { useUser } from "@stackframe/stack"; | ||
| import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises"; | ||
|
|
||
| export default function Page() { | ||
| const myAction = useAction(api.myActions.myAction); | ||
| const user = useUser({ or: "redirect" }); | ||
| const [data, setData] = useState<string | null>(null); | ||
|
|
||
|
|
||
| return ( | ||
| <div className="flex flex-col gap-8 max-w-lg mx-auto pt-10"> | ||
| <div className="flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md"> | ||
| <h2 className="text-xl font-bold">User read-only metadata</h2> | ||
| <code> | ||
| <pre>{JSON.stringify(user.clientReadOnlyMetadata, null, 2)}</pre> | ||
| </code> | ||
| </div> | ||
| <input type="text" placeholder="test 123" className="border border-slate-300 rounded-md p-2" onChange={(e) => setData(e.target.value)} /> | ||
| <button | ||
| className="bg-foreground text-background text-sm px-4 py-2 rounded-md" | ||
| onClick={() => { | ||
| runAsynchronouslyWithAlert(async () => { | ||
| await myAction({ testMetadata: data ?? "" }) | ||
| alert("User's client read-only metadata updated, refresh to see changes") | ||
| }) | ||
| }} | ||
| > | ||
| My Action | ||
| </button> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,8 @@ | ||||||||||||||||||||||||||||
| import Home from "./inner"; | ||||||||||||||||||||||||||||
| import { preloadQuery, preloadedQueryResult } from "convex/nextjs"; | ||||||||||||||||||||||||||||
| import { api } from "@/convex/_generated/api"; | ||||||||||||||||||||||||||||
| import { ConvexHttpClient } from "convex/browser"; | ||||||||||||||||||||||||||||
| import { stackServerApp } from "@/stack/server"; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export default async function ServerPage() { | ||||||||||||||||||||||||||||
| const preloaded = await preloadQuery(api.myFunctions.listNumbers, { | ||||||||||||||||||||||||||||
|
|
@@ -9,9 +11,20 @@ export default async function ServerPage() { | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const data = preloadedQueryResult(preloaded); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); | ||||||||||||||||||||||||||||
| const token = await stackServerApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" }); | ||||||||||||||||||||||||||||
| convex.setAuth(token); | ||||||||||||||||||||||||||||
| const userInfo = await convex.query(api.myFunctions.getUserInfo, {}); | ||||||||||||||||||||||||||||
|
Comment on lines
+14
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Add error handling for authentication and query operations. The token retrieval, authentication setup, and query execution lack error handling. If the token is empty (due to the change in Apply this diff to add error handling: - const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
- const token = await stackServerApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" });
- convex.setAuth(token);
- const userInfo = await convex.query(api.myFunctions.getUserInfo, {});
+ const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
+ const token = await stackServerApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" });
+
+ if (!token) {
+ throw new Error("Failed to obtain authentication token");
+ }
+
+ convex.setAuth(token);
+ const userInfo = await convex.query(api.myFunctions.getUserInfo, {});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||
| <main className="p-8 flex flex-col gap-4 mx-auto max-w-2xl"> | ||||||||||||||||||||||||||||
| <h1 className="text-4xl font-bold text-center">Convex + Next.js</h1> | ||||||||||||||||||||||||||||
| <div className="flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md"> | ||||||||||||||||||||||||||||
| <h2 className="text-xl font-bold">User info</h2> | ||||||||||||||||||||||||||||
| <code> | ||||||||||||||||||||||||||||
| <pre>{JSON.stringify(JSON.parse(userInfo), null, 2)}</pre> | ||||||||||||||||||||||||||||
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||
| </code> | ||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||
| <div className="flex flex-col gap-4 bg-slate-200 dark:bg-slate-800 p-4 rounded-md"> | ||||||||||||||||||||||||||||
| <h2 className="text-xl font-bold">Non-reactive server-loaded data</h2> | ||||||||||||||||||||||||||||
| <code> | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||||||||||||||
| import { api } from "@/convex/_generated/api"; | ||||||||||||||||||
| import { stackClientApp } from "@/stack/client"; | ||||||||||||||||||
| import { ConvexHttpClient, ConvexClient } from "convex/browser"; | ||||||||||||||||||
|
|
||||||||||||||||||
| const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential issue: Module-level environment variable access. Creating Consider moving client creation into the function or adding validation: -const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
-
export async function getUserInfoConvexHttpClient() {
+ const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
const token = await stackClientApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" });
convexHttpClient.setAuth(token);
const userInfo = await convexHttpClient.query(api.myFunctions.getUserInfo, {});
return userInfo;
}Or add validation if keeping module-level creation: +if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
+ throw new Error("NEXT_PUBLIC_CONVEX_URL environment variable is required");
+}
+
const convexHttpClient = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| export async function getUserInfoConvexHttpClient() { | ||||||||||||||||||
| const token = await stackClientApp.getConvexHttpClientAuth({ tokenStore: "nextjs-cookie" }); | ||||||||||||||||||
| convexHttpClient.setAuth(token); | ||||||||||||||||||
| const userInfo = await convexHttpClient.query(api.myFunctions.getUserInfo, {}); | ||||||||||||||||||
| return userInfo; | ||||||||||||||||||
| } | ||||||||||||||||||
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| const convexClient = new ConvexClient(process.env.NEXT_PUBLIC_CONVEX_URL!); | ||||||||||||||||||
| convexClient.setAuth(stackClientApp.getConvexClientAuth({ tokenStore: "nextjs-cookie" })) | ||||||||||||||||||
|
|
||||||||||||||||||
| export async function getUserInfoConvexClient() { | ||||||||||||||||||
| const userInfo = await convexClient.query(api.myFunctions.getUserInfo, {}); | ||||||||||||||||||
| return userInfo; | ||||||||||||||||||
| } | ||||||||||||||||||
BilalG1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| "use node" | ||
|
|
||
| import { v } from "convex/values"; | ||
| import { stackServerApp } from "../stack/server"; | ||
| import { action } from "./_generated/server"; | ||
|
|
||
|
|
||
| export const myAction = action({ | ||
| args: { | ||
| testMetadata: v.string(), | ||
| }, | ||
|
|
||
| handler: async (ctx, args) => { | ||
| const partialUser = await stackServerApp.getPartialUser({ from: "convex", ctx }); | ||
| if (!partialUser) { | ||
| return null; | ||
| } | ||
| const user = await stackServerApp.getUser(partialUser?.id); | ||
| if (!user) { | ||
| return null; | ||
| } | ||
| await user.setClientReadOnlyMetadata({ | ||
| test: args.testMetadata, | ||
| }) | ||
| }, | ||
| }); | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1763,7 +1763,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec | |||||
| async getConvexHttpClientAuth(options: { tokenStore: TokenStoreInit }): Promise<string> { | ||||||
| const session = await this._getSession(options.tokenStore); | ||||||
| const tokens = await session.getOrFetchLikelyValidTokens(20_000); | ||||||
| return tokens?.accessToken.token ?? throwErr("No access token available"); | ||||||
| return tokens?.accessToken.token ?? ""; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Changed error handling behavior from throwing an error to returning empty string when no access token is available, which can cause silent failures in server-side authentication. View DetailsAnalysisSilent authentication failure in getConvexHttpClientAuth() methodWhat fails: How to reproduce:
Result: Authentication failures become silent - instead of clear error indicating missing token, Convex receives empty string and treats user as unauthenticated without obvious indication Expected: Method should throw clear error "No access token available" as originally implemented, making authentication issues immediately apparent for debugging Evidence: Git commit |
||||||
| } | ||||||
|
Comment on lines
1763
to
1767
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify that all callers handle empty auth strings correctly. This change modifies error signaling from throwing an exception to returning an empty string when no access token is available. Callers that previously relied on catching exceptions for missing tokens will now receive an empty string, which could lead to silent authentication failures if not properly handled downstream. Run the following script to find all usages of Additionally, check if Convex clients properly handle empty auth strings or if they require non-empty tokens: 🏁 Script executed: #!/bin/bash
# Search for all call sites of getConvexHttpClientAuth
rg -n -C5 'getConvexHttpClientAuth\s*\(' --type=ts --type=tsxLength of output: 27 🌐 Web query: 💡 Result: Short answer: setAuth accepts a string but an empty string is not treated as "no token" — it will result in an Authorization: Bearer header (and the backend will reject it). Use clearAuth() (or pass null/undefined where supported by that API) to remove auth. [1][2] Sources:
#!/bin/bash
# Search for all call sites of getConvexHttpClientAuth across the repo
rg -n -C5 'getConvexHttpClientAuth\s*\(' .#!/bin/bash
# Search for all call sites of getConvexHttpClientAuth across the repo
rg -n -C5 'getConvexHttpClientAuth\s*\(' .🏁 Script executed: #!/bin/bash
# Search for all call sites of getConvexHttpClientAuth across the repo
rg -n -C5 'getConvexHttpClientAuth\s*\(' .Length of output: 6283 Handle empty auth strings in all callers 🤖 Prompt for AI Agents |
||||||
|
|
||||||
| protected async _updateClientUser(update: UserUpdateOptions, session: InternalSession) { | ||||||
|
|
||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.