diff --git a/users-and-descope/.gitignore b/users-and-descope/.gitignore new file mode 100644 index 0000000..bbb72ae --- /dev/null +++ b/users-and-descope/.gitignore @@ -0,0 +1,4 @@ +node_modules +.DS_Store +dist +.env.local diff --git a/users-and-descope/README.md b/users-and-descope/README.md new file mode 100644 index 0000000..639bf5a --- /dev/null +++ b/users-and-descope/README.md @@ -0,0 +1,17 @@ +# Users and Authentication Example App + +This example demonstrates how to add users and authentication to a basic chat +app. It uses [Descope](https://descope.com) for authentication. + +Users are initially presented with a "Log In" button. After user's log in, their +information is persisted to a `users` table. When users send messages, each +message is associated with the user that sent it. Lastly, users can log out with +a "Log Out" button. + +## Running the App + +Because this app uses authentication, it requires a bit of an additional setup. + +Follow these instructions https://docs.descope.com/frameworks/convex/ to setup Descope with +Convex. You will have to update the client in `main.tsx` and the server in +`auth.config.js`. diff --git a/users-and-descope/convex/README.md b/users-and-descope/convex/README.md new file mode 100644 index 0000000..b317aef --- /dev/null +++ b/users-and-descope/convex/README.md @@ -0,0 +1,90 @@ +# Welcome to your Convex functions directory! + +Write your Convex functions here. +See https://docs.convex.dev/functions for more. + +A query function that takes two arguments looks like: + +```ts +// functions.js +import { query } from "./_generated/server"; +import { v } from "convex/values"; + +export const myQueryFunction = query({ + // Validators for arguments. + args: { + first: v.number(), + second: v.string(), + }, + + // Function implementation. + handler: async (ctx, args) => { + // Read the database as many times as you need here. + // See https://docs.convex.dev/database/reading-data. + const documents = await ctx.db.query("tablename").collect(); + + // Arguments passed from the client are properties of the args object. + console.log(args.first, args.second); + + // Write arbitrary JavaScript here: filter, aggregate, build derived data, + // remove non-public properties, or create new objects. + return documents; + }, +}); +``` + +Using this query function in a React component looks like: + +```ts +const data = useQuery(api.functions.myQueryFunction, { + first: 10, + second: "hello", +}); +``` + +A mutation function looks like: + +```ts +// functions.js +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export const myMutationFunction = mutation({ + // Validators for arguments. + args: { + first: v.string(), + second: v.string(), + }, + + // Function implementation. + handler: async (ctx, args) => { + // Insert or modify documents in the database here. + // Mutations can also read from the database like queries. + // See https://docs.convex.dev/database/writing-data. + const message = { body: args.first, author: args.second }; + const id = await ctx.db.insert("messages", message); + + // Optionally, return a value from your mutation. + return await ctx.db.get(id); + }, +}); +``` + +Using this mutation function in a React component looks like: + +```ts +const mutation = useMutation(api.functions.myMutationFunction); +function handleButtonPress() { + // fire and forget, the most common way to use mutations + mutation({ first: "Hello!", second: "me" }); + // OR + // use the result once the mutation has completed + mutation({ first: "Hello!", second: "me" }).then((result) => + console.log(result) + ); +} +``` + +Use the Convex CLI to push your functions to a deployment. See everything +the Convex CLI can do by running `npx convex -h` in your project root +directory. To learn more, launch the docs with `npx convex docs`. diff --git a/users-and-descope/convex/_generated/api.d.ts b/users-and-descope/convex/_generated/api.d.ts new file mode 100644 index 0000000..c766103 --- /dev/null +++ b/users-and-descope/convex/_generated/api.d.ts @@ -0,0 +1,39 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.10.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + ApiFromModules, + FilterApi, + FunctionReference, +} from "convex/server"; +import type * as messages from "../messages.js"; +import type * as users from "../users.js"; + +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +declare const fullApi: ApiFromModules<{ + messages: typeof messages; + users: typeof users; +}>; +export declare const api: FilterApi< + typeof fullApi, + FunctionReference +>; +export declare const internal: FilterApi< + typeof fullApi, + FunctionReference +>; diff --git a/users-and-descope/convex/_generated/api.js b/users-and-descope/convex/_generated/api.js new file mode 100644 index 0000000..6898878 --- /dev/null +++ b/users-and-descope/convex/_generated/api.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.10.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import { anyApi } from "convex/server"; + +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export const api = anyApi; +export const internal = anyApi; diff --git a/users-and-descope/convex/_generated/dataModel.d.ts b/users-and-descope/convex/_generated/dataModel.d.ts new file mode 100644 index 0000000..267aafc --- /dev/null +++ b/users-and-descope/convex/_generated/dataModel.d.ts @@ -0,0 +1,61 @@ +/* eslint-disable */ +/** + * Generated data model types. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.10.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + DataModelFromSchemaDefinition, + DocumentByName, + TableNamesInDataModel, + SystemTableNames, +} from "convex/server"; +import type { GenericId } from "convex/values"; +import schema from "../schema.js"; + +/** + * The names of all of your Convex tables. + */ +export type TableNames = TableNamesInDataModel; + +/** + * The type of a document stored in Convex. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Doc = DocumentByName< + DataModel, + TableName +>; + +/** + * An identifier for a document in Convex. + * + * Convex documents are uniquely identified by their `Id`, which is accessible + * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). + * + * Documents can be loaded using `db.get(id)` in query and mutation functions. + * + * IDs are just strings at runtime, but this type can be used to distinguish them from other + * strings when type checking. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Id = + GenericId; + +/** + * A type describing your Convex data model. + * + * This type includes information about what tables you have, the type of + * documents stored in those tables, and the indexes defined on them. + * + * This type is used to parameterize methods like `queryGeneric` and + * `mutationGeneric` to make them type-safe. + */ +export type DataModel = DataModelFromSchemaDefinition; diff --git a/users-and-descope/convex/_generated/server.d.ts b/users-and-descope/convex/_generated/server.d.ts new file mode 100644 index 0000000..e38e64f --- /dev/null +++ b/users-and-descope/convex/_generated/server.d.ts @@ -0,0 +1,143 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.10.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + ActionBuilder, + HttpActionBuilder, + MutationBuilder, + QueryBuilder, + GenericActionCtx, + GenericMutationCtx, + GenericQueryCtx, + GenericDatabaseReader, + GenericDatabaseWriter, +} from "convex/server"; +import type { DataModel } from "./dataModel.js"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const query: QueryBuilder; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const internalQuery: QueryBuilder; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const mutation: MutationBuilder; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const internalMutation: MutationBuilder; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export declare const action: ActionBuilder; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export declare const internalAction: ActionBuilder; + +/** + * Define an HTTP action. + * + * This function will be used to respond to HTTP requests received by a Convex + * deployment if the requests matches the path and method where this action + * is routed. Be sure to route your action in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export declare const httpAction: HttpActionBuilder; + +/** + * A set of services for use within Convex query functions. + * + * The query context is passed as the first argument to any Convex query + * function run on the server. + * + * This differs from the {@link MutationCtx} because all of the services are + * read-only. + */ +export type QueryCtx = GenericQueryCtx; + +/** + * A set of services for use within Convex mutation functions. + * + * The mutation context is passed as the first argument to any Convex mutation + * function run on the server. + */ +export type MutationCtx = GenericMutationCtx; + +/** + * A set of services for use within Convex action functions. + * + * The action context is passed as the first argument to any Convex action + * function run on the server. + */ +export type ActionCtx = GenericActionCtx; + +/** + * An interface to read from the database within Convex query functions. + * + * The two entry points are {@link DatabaseReader.get}, which fetches a single + * document by its {@link Id}, or {@link DatabaseReader.query}, which starts + * building a query. + */ +export type DatabaseReader = GenericDatabaseReader; + +/** + * An interface to read from and write to the database within Convex mutation + * functions. + * + * Convex guarantees that all writes within a single mutation are + * executed atomically, so you never have to worry about partial writes leaving + * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) + * for the guarantees Convex provides your functions. + */ +export type DatabaseWriter = GenericDatabaseWriter; diff --git a/users-and-descope/convex/_generated/server.js b/users-and-descope/convex/_generated/server.js new file mode 100644 index 0000000..31adbda --- /dev/null +++ b/users-and-descope/convex/_generated/server.js @@ -0,0 +1,90 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * Generated by convex@1.10.0. + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + actionGeneric, + httpActionGeneric, + queryGeneric, + mutationGeneric, + internalActionGeneric, + internalMutationGeneric, + internalQueryGeneric, +} from "convex/server"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const query = queryGeneric; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const internalQuery = internalQueryGeneric; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const mutation = mutationGeneric; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const internalMutation = internalMutationGeneric; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export const action = actionGeneric; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export const internalAction = internalActionGeneric; + +/** + * Define a Convex HTTP action. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object + * as its second. + * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. + */ +export const httpAction = httpActionGeneric; diff --git a/users-and-descope/convex/auth.config.js b/users-and-descope/convex/auth.config.js new file mode 100644 index 0000000..4c31cf7 --- /dev/null +++ b/users-and-descope/convex/auth.config.js @@ -0,0 +1,11 @@ +export default { + providers: [ + { + // Replace with your own Descope Issuer URL. + // This will be your Descope Project ID or `https://api.descope.com/YOUR_DESCOPE_PROJECT_ID` + // depending on if you've enabled AWS API Gateway compliant JWTs in your Project Settings + domain: "P2OkfVnJi5Ht7mpCqHjx17nV5epH", + applicationID: "convex", + }, + ], +}; diff --git a/users-and-descope/convex/messages.ts b/users-and-descope/convex/messages.ts new file mode 100644 index 0000000..07c2970 --- /dev/null +++ b/users-and-descope/convex/messages.ts @@ -0,0 +1,46 @@ +import { v } from "convex/values"; +import { query } from "./_generated/server"; +import { mutation } from "./_generated/server"; + +export const send = mutation({ + args: { body: v.string() }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) { + throw new Error("Unauthenticated call to mutation"); + } + // Note: If you don't want to define an index right away, you can use + // ctx.db.query("users") + // .filter(q => q.eq(q.field("tokenIdentifier"), identity.tokenIdentifier)) + // .unique(); + const user = await ctx.db + .query("users") + .withIndex("by_token", (q) => + q.eq("tokenIdentifier", identity.tokenIdentifier), + ) + .unique(); + if (!user) { + throw new Error("Unauthenticated call to mutation"); + } + + await ctx.db.insert("messages", { body: args.body, user: user._id }); + }, +}); + +export const list = query({ + args: {}, + handler: async (ctx) => { + const messages = await ctx.db.query("messages").collect(); + return Promise.all( + messages.map(async (message) => { + // For each message in this channel, fetch the `User` who wrote it and + // insert their name into the `author` field. + const user = await ctx.db.get(message.user); + return { + author: user!.name, + ...message, + }; + }), + ); + }, +}); diff --git a/users-and-descope/convex/schema.ts b/users-and-descope/convex/schema.ts new file mode 100644 index 0000000..b753847 --- /dev/null +++ b/users-and-descope/convex/schema.ts @@ -0,0 +1,13 @@ +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +export default defineSchema({ + messages: defineTable({ + body: v.string(), + user: v.id("users"), + }), + users: defineTable({ + name: v.string(), + tokenIdentifier: v.string(), + }).index("by_token", ["tokenIdentifier"]), +}); diff --git a/users-and-descope/convex/tsconfig.json b/users-and-descope/convex/tsconfig.json new file mode 100644 index 0000000..d067e20 --- /dev/null +++ b/users-and-descope/convex/tsconfig.json @@ -0,0 +1,24 @@ +{ + /* This TypeScript project config describes the environment that + * Convex functions run in and is used to typecheck them. + * You can modify it, but some settings required to use Convex. + */ + "compilerOptions": { + /* These settings are not required by Convex and can be modified. */ + "allowJs": true, + "strict": true, + + /* These compiler options are required by Convex */ + "target": "ESNext", + "lib": ["ES2021", "dom"], + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "module": "ESNext", + "moduleResolution": "Node", + "isolatedModules": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["./**/*"], + "exclude": ["./_generated"] +} diff --git a/users-and-descope/convex/users.ts b/users-and-descope/convex/users.ts new file mode 100644 index 0000000..0ff7f76 --- /dev/null +++ b/users-and-descope/convex/users.ts @@ -0,0 +1,43 @@ +import { mutation } from "./_generated/server"; + +/** + * Insert or update the user in a Convex table then return the document's ID. + * + * The `UserIdentity.tokenIdentifier` string is a stable and unique value we use + * to look up identities. + * + * Keep in mind that `UserIdentity` has a number of optional fields, the + * presence of which depends on the identity provider chosen. It's up to the + * application developer to determine which ones are available and to decide + * which of those need to be persisted. For Descope the fields are determined + * by the JWT token's Claims config. + */ +export const store = mutation({ + args: {}, + handler: async (ctx) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) { + throw new Error("Called storeUser without authentication present"); + } + + // Check if we've already stored this identity before. + const user = await ctx.db + .query("users") + .withIndex("by_token", (q) => + q.eq("tokenIdentifier", identity.tokenIdentifier), + ) + .unique(); + if (user !== null) { + // If we've seen this identity before but the name has changed, patch the value. + if (user.name !== identity.name) { + await ctx.db.patch(user._id, { name: identity.name }); + } + return user._id; + } + // If it's a new identity, create a new `User`. + return await ctx.db.insert("users", { + name: identity.name!, + tokenIdentifier: identity.tokenIdentifier, + }); + }, +}); diff --git a/users-and-descope/index.html b/users-and-descope/index.html new file mode 100644 index 0000000..133391e --- /dev/null +++ b/users-and-descope/index.html @@ -0,0 +1,12 @@ + + + + + + Convex Chat + + +
+ + + diff --git a/users-and-descope/package.json b/users-and-descope/package.json new file mode 100644 index 0000000..fe9e380 --- /dev/null +++ b/users-and-descope/package.json @@ -0,0 +1,28 @@ +{ + "name": "users-and-descope", + "version": "0.0.0", + "scripts": { + "dev": "npm-run-all --parallel dev:server dev:client", + "build": "vite build", + "dev:server": "convex dev", + "dev:client": "vite --open", + "predev": "convex dev --until-success" + }, + "dependencies": { + "convex": "1.10.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "@descope/react-sdk": "2.0.10", + "prettier": "2.8.8" + }, + "devDependencies": { + "@types/babel__core": "^7.20.0", + "@types/node": "^16.11.12", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "^4.1.1", + "typescript": "~5.0.3", + "vite": "4.5.2", + "npm-run-all": "^4.1.5" + } +} diff --git a/users-and-descope/src/App.tsx b/users-and-descope/src/App.tsx new file mode 100644 index 0000000..650bae9 --- /dev/null +++ b/users-and-descope/src/App.tsx @@ -0,0 +1,58 @@ +import { FormEvent, useState } from "react"; +import { useMutation, useQuery } from "convex/react"; +import { api } from "../convex/_generated/api"; +import Badge from "./Badge"; +import { getServerSession, useSession, useDescope } from "@descope/react-sdk"; +import useStoreUserEffect from "./useStoreUserEffect"; +import { useCallback } from "react" + +export default function App() { + const userId = useStoreUserEffect(); + + const messages = useQuery(api.messages.list) || []; + + const [newMessageText, setNewMessageText] = useState(""); + const sendMessage = useMutation(api.messages.send); + + const sdk = useDescope(); + + const handleLogout = useCallback(() => { + sdk.logout(); + }, [sdk]); + + async function handleSendMessage(event: FormEvent) { + event.preventDefault(); + await sendMessage({ body: newMessageText }); + setNewMessageText(""); + } + return ( +
+

Convex Chat

+ +

+ +

+
    + {messages.map((message) => ( +
  • + {message.author}: + {message.body} + {new Date(message._creationTime).toLocaleTimeString()} +
  • + ))} +
+
+ setNewMessageText(event.target.value)} + placeholder="Write a message…" + /> + +
+
+ ); +} diff --git a/users-and-descope/src/Badge.tsx b/users-and-descope/src/Badge.tsx new file mode 100644 index 0000000..93f1f55 --- /dev/null +++ b/users-and-descope/src/Badge.tsx @@ -0,0 +1,11 @@ +import { useUser } from "@descope/react-sdk"; + +export default function Badge() { + const { user } = useUser(); + + return ( +

+ Logged in{user!.name ? ` as ${user!.name}` : ""} +

+ ); +} diff --git a/users-and-descope/src/LoginPage.tsx b/users-and-descope/src/LoginPage.tsx new file mode 100644 index 0000000..44151dc --- /dev/null +++ b/users-and-descope/src/LoginPage.tsx @@ -0,0 +1,12 @@ +import { Descope } from "@descope/react-sdk"; + +export default function LoginPage() { + return ( +
+

Convex Chat

+

+ +

+
+ ); +} diff --git a/users-and-descope/src/index.css b/users-and-descope/src/index.css new file mode 100644 index 0000000..4df054f --- /dev/null +++ b/users-and-descope/src/index.css @@ -0,0 +1,116 @@ +/* reset */ +* { + margin: 0; + padding: 0; + border: 0; + line-height: 1.5; +} + +body { + font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", helvetica, + sans-serif; +} + +main { + padding-top: 1em; + padding-bottom: 1em; + width: min(800px, 95vw); + margin: 0 auto; +} + +h1 { + text-align: center; + margin-bottom: 8px; + font-size: 1.8em; + font-weight: 500; +} + +h2 { + text-align: center; + margin-bottom: 8px; +} + +.badge { + text-align: center; + margin-bottom: 16px; +} +.badge span { + background-color: #212529; + color: #ffffff; + border-radius: 6px; + font-weight: bold; + padding: 4px 8px 4px 8px; + font-size: 0.75em; +} + +ul { + margin: 8px; + border-radius: 8px; + border: solid 1px lightgray; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +ul:empty { + display: none; +} + +li { + display: flex; + justify-content: flex-start; + padding: 8px 16px 8px 16px; + border-bottom: solid 1px lightgray; + font-size: 16px; +} + +li:last-child { + border: 0; +} + +li span:nth-child(1) { + font-weight: bold; + margin-right: 4px; + white-space: nowrap; +} +li span:nth-child(2) { + margin-right: 4px; + word-break: break-word; +} +li span:nth-child(3) { + color: #6c757d; + margin-left: auto; + white-space: nowrap; +} + +form { + display: flex; + justify-content: center; +} + +input:not([type]) { + padding: 6px 12px 6px 12px; + color: rgb(33, 37, 41); + border: solid 1px rgb(206, 212, 218); + border-radius: 8px; + font-size: 16px; +} + +input[type="submit"], +button { + margin-left: 4px; + background: lightblue; + color: white; + padding: 6px 12px 6px 12px; + border-radius: 8px; + font-size: 16px; + background-color: rgb(49, 108, 244); +} + +input[type="submit"]:hover, +button:hover { + background-color: rgb(41, 93, 207); +} + +input[type="submit"]:disabled, +button:disabled { + background-color: rgb(122, 160, 248); +} diff --git a/users-and-descope/src/main.tsx b/users-and-descope/src/main.tsx new file mode 100644 index 0000000..994db54 --- /dev/null +++ b/users-and-descope/src/main.tsx @@ -0,0 +1,33 @@ +import { StrictMode } from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; +import LoginPage from "./LoginPage"; +import { + ConvexReactClient, + Authenticated, + Unauthenticated, +} from "convex/react"; +import { ConvexProviderWithDescope } from "convex/react-descope"; +import { AuthProvider } from "@descope/react-sdk"; + +const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + + + + + + + , +); diff --git a/users-and-descope/src/useStoreUserEffect.ts b/users-and-descope/src/useStoreUserEffect.ts new file mode 100644 index 0000000..208381f --- /dev/null +++ b/users-and-descope/src/useStoreUserEffect.ts @@ -0,0 +1,35 @@ +import { useUser } from "@descope/react-sdk"; +import { useConvexAuth } from "convex/react"; +import { useEffect, useState } from "react"; +import { useMutation } from "convex/react"; +import { api } from "../convex/_generated/api"; +import { Id } from "../convex/_generated/dataModel"; + +export default function useStoreUserEffect() { + const { isAuthenticated } = useConvexAuth(); + const { user } = useUser(); + // When this state is set we know the server + // has stored the user. + const [userId, setUserId] = useState | null>(null); + const storeUser = useMutation(api.users.store); + // Call the `storeUser` mutation function to store + // the current user in the `users` table and return the `Id` value. + useEffect(() => { + // If the user is not logged in don't do anything + if (!isAuthenticated) { + return; + } + // Store the user in the database. + // Recall that `storeUser` gets the user information via the `auth` + // object on the server. You don't need to pass anything manually here. + async function createUser() { + const id = await storeUser(); + setUserId(id); + } + createUser(); + return () => setUserId(null); + // Make sure the effect reruns if the user logs in with + // a different identity + }, [isAuthenticated, storeUser, user?.sub]); + return userId; +} diff --git a/users-and-descope/src/vite-env.d.ts b/users-and-descope/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/users-and-descope/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/users-and-descope/tsconfig.json b/users-and-descope/tsconfig.json new file mode 100644 index 0000000..f7cc85e --- /dev/null +++ b/users-and-descope/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["./src", "vite.config.ts"] +} diff --git a/users-and-descope/users-and-descope.build.cache.log b/users-and-descope/users-and-descope.build.cache.log new file mode 100644 index 0000000..ac4426a --- /dev/null +++ b/users-and-descope/users-and-descope.build.cache.log @@ -0,0 +1 @@ +Project does not have a rush-project.json configuration file, or one provided by a rig, so it does not support caching. diff --git a/users-and-descope/vite.config.ts b/users-and-descope/vite.config.ts new file mode 100644 index 0000000..9cc50ea --- /dev/null +++ b/users-and-descope/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +});