28 |
29 |
30 |
31 | );
32 |
--------------------------------------------------------------------------------
/convex/_generated/api.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated `api` utility.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * Generated by convex@1.0.2.
8 | * To regenerate, run `npx convex dev`.
9 | * @module
10 | */
11 |
12 | import type {
13 | ApiFromModules,
14 | FilterApi,
15 | FunctionReference,
16 | } from "convex/server";
17 | import type * as crons from "../crons";
18 | import type * as lucia from "../lucia";
19 | import type * as users from "../users";
20 | import type * as withAuth from "../withAuth";
21 |
22 | /**
23 | * A utility for referencing Convex functions in your app's API.
24 | *
25 | * Usage:
26 | * ```js
27 | * const myFunctionReference = api.myModule.myFunction;
28 | * ```
29 | */
30 | declare const fullApi: ApiFromModules<{
31 | crons: typeof crons;
32 | lucia: typeof lucia;
33 | users: typeof users;
34 | withAuth: typeof withAuth;
35 | }>;
36 | export declare const api: FilterApi<
37 | typeof fullApi,
38 | FunctionReference
39 | >;
40 | export declare const internal: FilterApi<
41 | typeof fullApi,
42 | FunctionReference
43 | >;
44 |
--------------------------------------------------------------------------------
/convex/schema.ts:
--------------------------------------------------------------------------------
1 | import { defineSchema, defineTable } from "convex/server";
2 | import { Validator, v } from "convex/values";
3 |
4 | export default defineSchema({
5 | ...authTables({
6 | user: { email: v.string() },
7 | session: {},
8 | }),
9 | });
10 |
11 | function authTables<
12 | UserFields extends Record>,
13 | SchemaFields extends Record>
14 | >({ user, session }: { user: UserFields; session: SchemaFields }) {
15 | return {
16 | users: defineTable({
17 | ...user,
18 | id: v.string(),
19 | }).index("byId", ["id"]),
20 | sessions: defineTable({
21 | ...session,
22 | id: v.string(),
23 | user_id: v.string(),
24 | active_expires: v.float64(),
25 | idle_expires: v.float64(),
26 | })
27 | // `as any` because TypeScript can't infer the table fields correctly
28 | .index("byId", ["id" as any])
29 | .index("byUserId", ["user_id" as any]),
30 | auth_keys: defineTable({
31 | id: v.string(),
32 | hashed_password: v.union(v.string(), v.null()),
33 | user_id: v.string(),
34 | })
35 | .index("byId", ["id"])
36 | .index("byUserId", ["user_id"]),
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lucia-full-test",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "npm-run-all dev:init --parallel dev:frontend dev:backend",
8 | "dev:backend": "convex dev",
9 | "dev:frontend": "vite --open --clearScreen false",
10 | "dev:init": "convex dev --until-success",
11 | "build": "tsc && vite build",
12 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
13 | "preview": "vite preview"
14 | },
15 | "dependencies": {
16 | "@types/node": "^20.4.6",
17 | "convex": "^1.0.2",
18 | "crypto-js": "^4.1.1",
19 | "lucia": "^2.0.0",
20 | "npm-run-all": "^4.1.5",
21 | "react": "^18.2.0",
22 | "react-dom": "^18.2.0"
23 | },
24 | "devDependencies": {
25 | "@types/crypto-js": "^4.1.1",
26 | "@types/react": "^18.2.15",
27 | "@types/react-dom": "^18.2.7",
28 | "@typescript-eslint/eslint-plugin": "^6.0.0",
29 | "@typescript-eslint/parser": "^6.0.0",
30 | "@vitejs/plugin-react": "^4.0.3",
31 | "eslint": "^8.45.0",
32 | "eslint-plugin-react-hooks": "^4.6.0",
33 | "eslint-plugin-react-refresh": "^0.4.3",
34 | "typescript": "^5.0.2",
35 | "vite": "^4.4.5"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/SessionProvider.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-refresh/only-export-components */
2 |
3 | import { createContext, useCallback, useContext, useState } from "react";
4 |
5 | const SessionContext = createContext(undefined as any);
6 |
7 | export function useSessionId(): string | null {
8 | return useContext(SessionContext).sessionId;
9 | }
10 |
11 | export function useSetSessionId(): (sessionId: string | null) => void {
12 | return useContext(SessionContext).setSessionId;
13 | }
14 |
15 | export function SessionProvider({ children }: { children: React.ReactNode }) {
16 | const [sessionId, setSessionIdState] = useState(getSavedSessionId());
17 | const setSessionId = useCallback(
18 | (value: string | null) => {
19 | setSavedSessionId(value);
20 | setSessionIdState(value);
21 | },
22 | [setSessionIdState]
23 | );
24 | return (
25 |
26 | {children}
27 |
28 | );
29 | }
30 |
31 | function getSavedSessionId() {
32 | return localStorage.getItem("sessionId");
33 | }
34 |
35 | export function setSavedSessionId(sessionId: string | null) {
36 | if (sessionId == null) {
37 | localStorage.removeItem("sessionId");
38 | } else {
39 | localStorage.setItem("sessionId", sessionId);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/usingSession.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | useQuery as useConvexQuery,
3 | useMutation as useConvexMutation,
4 | } from "convex/react";
5 | import { ReactMutation } from "convex/react-internal";
6 | import { FunctionReference } from "convex/server";
7 | import { useCallback } from "react";
8 | import { useSessionId } from "./SessionProvider";
9 |
10 | export function useQuery<
11 | Args extends { sessionId: string | null },
12 | Query extends FunctionReference<"query", "public", Args>
13 | >(
14 | query: Query,
15 | args: Omit
16 | ): Query["_returnType"] | undefined {
17 | const sessionId = useSessionId();
18 | return useConvexQuery(query, { ...args, sessionId } as any);
19 | }
20 |
21 | export function useMutation<
22 | Args extends { sessionId: string | null },
23 | Mutation extends FunctionReference<"mutation", "public", Args>
24 | >(
25 | mutation: Mutation
26 | ): ReactMutation<
27 | FunctionReference<"mutation", "public", Omit>
28 | > {
29 | const doMutation = useConvexMutation(mutation);
30 | const sessionId = useSessionId();
31 | return useCallback(
32 | (args: Omit) => {
33 | return doMutation({ ...args, sessionId } as any);
34 | },
35 | [doMutation, sessionId]
36 | ) as any; // We don't support optimistic updates
37 | }
38 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/convex/users.ts:
--------------------------------------------------------------------------------
1 | import { v } from "convex/values";
2 | import { queryWithAuth, mutationWithAuth } from "./withAuth";
3 | import { Id } from "./_generated/dataModel";
4 |
5 | export const get = queryWithAuth({
6 | args: {},
7 | handler: async (ctx) => {
8 | return ctx.session?.user;
9 | },
10 | });
11 |
12 | export const signIn = mutationWithAuth({
13 | args: {
14 | email: v.string(),
15 | password: v.string(),
16 | },
17 | handler: async (ctx, { email, password }) => {
18 | const key = await ctx.auth.useKey("password", email, password);
19 | const session = await ctx.auth.createSession({
20 | userId: key.userId,
21 | attributes: {
22 | // These will be filled out by Convex
23 | _id: "" as Id<"sessions">,
24 | _creationTime: 0,
25 | },
26 | });
27 | return session.sessionId;
28 | },
29 | });
30 |
31 | export const signUp = mutationWithAuth({
32 | args: {
33 | email: v.string(),
34 | password: v.string(),
35 | },
36 | handler: async (ctx, { email, password }) => {
37 | const user = await ctx.auth.createUser({
38 | key: {
39 | password: password,
40 | providerId: "password",
41 | providerUserId: email,
42 | },
43 | attributes: {
44 | email,
45 | // These will be filled out by Convex
46 | _id: "" as Id<"users">,
47 | _creationTime: 0,
48 | },
49 | });
50 | const session = await ctx.auth.createSession({
51 | userId: user.userId,
52 | attributes: {
53 | // These will be filled out by Convex
54 | _id: "" as Id<"sessions">,
55 | _creationTime: 0,
56 | },
57 | });
58 | return session.sessionId;
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/src/LoginForm.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useSetSessionId } from "./SessionProvider";
3 | import { useMutation } from "./usingSession";
4 | import { api } from "../convex/_generated/api";
5 |
6 | export function LoginForm() {
7 | const setSessionId = useSetSessionId();
8 | const [flow, setFlow] = useState<"signIn" | "signUp">("signIn");
9 | const signIn = useMutation(api.users.signIn);
10 | const signUp = useMutation(api.users.signUp);
11 |
12 | const handleSubmit = async (event: React.FormEvent) => {
13 | event.preventDefault();
14 | const data = new FormData(event.currentTarget);
15 | try {
16 | const sessionId = await (flow === "signIn" ? signIn : signUp)({
17 | email: (data.get("email") as string | null) ?? "",
18 | password: (data.get("password") as string | null) ?? "",
19 | });
20 | setSessionId(sessionId);
21 | } catch {
22 | alert(
23 | flow === "signIn" ? "Invalid email or password" : "Email already in use"
24 | );
25 | }
26 | };
27 | return (
28 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/convex/_generated/dataModel.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated data model types.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * Generated by convex@1.0.2.
8 | * To regenerate, run `npx convex dev`.
9 | * @module
10 | */
11 |
12 | import type { DataModelFromSchemaDefinition } from "convex/server";
13 | import type { DocumentByName, TableNamesInDataModel } from "convex/server";
14 | import type { GenericId } from "convex/values";
15 | import schema from "../schema";
16 |
17 | /**
18 | * The names of all of your Convex tables.
19 | */
20 | export type TableNames = TableNamesInDataModel;
21 |
22 | /**
23 | * The type of a document stored in Convex.
24 | *
25 | * @typeParam TableName - A string literal type of the table name (like "users").
26 | */
27 | export type Doc = DocumentByName<
28 | DataModel,
29 | TableName
30 | >;
31 |
32 | /**
33 | * An identifier for a document in Convex.
34 | *
35 | * Convex documents are uniquely identified by their `Id`, which is accessible
36 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
37 | *
38 | * Documents can be loaded using `db.get(id)` in query and mutation functions.
39 | *
40 | * IDs are just strings at runtime, but this type can be used to distinguish them from other
41 | * strings when type checking.
42 | *
43 | * @typeParam TableName - A string literal type of the table name (like "users").
44 | */
45 | export type Id = GenericId;
46 |
47 | /**
48 | * A type describing your Convex data model.
49 | *
50 | * This type includes information about what tables you have, the type of
51 | * documents stored in those tables, and the indexes defined on them.
52 | *
53 | * This type is used to parameterize methods like `queryGeneric` and
54 | * `mutationGeneric` to make them type-safe.
55 | */
56 | export type DataModel = DataModelFromSchemaDefinition;
57 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | -webkit-text-size-adjust: 100%;
15 | }
16 |
17 | a {
18 | font-weight: 500;
19 | color: #646cff;
20 | text-decoration: inherit;
21 | }
22 | a:hover {
23 | color: #535bf2;
24 | }
25 |
26 | body {
27 | margin: 0;
28 | display: flex;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | }
32 |
33 | h1 {
34 | font-size: 3.2em;
35 | line-height: 1.1;
36 | }
37 |
38 | input[type="submit"],
39 | button {
40 | font-size: 1em;
41 | font-weight: 500;
42 | font-family: inherit;
43 | background-color: #1a1a1a;
44 | cursor: pointer;
45 | transition: border-color 0.25s;
46 | }
47 |
48 | input[type="submit"],
49 | button,
50 | input {
51 | padding: 0.8em 1.2em;
52 | font-size: 1em;
53 | border-radius: 4px;
54 | border: 1px solid rgb(82, 82, 82);
55 | }
56 |
57 | input[type="submit"]:hover,
58 | button:hover {
59 | border-color: #646cff;
60 | }
61 | input[type="submit"]:focus,
62 | input[type="submit"]:focus-visible,
63 | button:focus,
64 | button:focus-visible {
65 | outline: 4px auto -webkit-focus-ring-color;
66 | }
67 |
68 | form {
69 | display: flex;
70 | flex-direction: column;
71 | justify-content: center;
72 | flex-grow: 1;
73 | gap: 0.2em;
74 | width: 19em;
75 | }
76 |
77 | label {
78 | text-align: left;
79 | }
80 |
81 | input {
82 | margin-bottom: 1em;
83 | }
84 |
85 | @media (prefers-color-scheme: light) {
86 | :root {
87 | color: #213547;
88 | background-color: #ffffff;
89 | }
90 | a:hover {
91 | color: #747bff;
92 | }
93 | input[type="submit"],
94 | button {
95 | background-color: #f9f9f9;
96 | }
97 | input[type="submit"],
98 | button,
99 | input {
100 | border-color: rgb(195, 195, 195);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/convex/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your Convex functions directory!
2 |
3 | Write your Convex functions here. See
4 | https://docs.convex.dev/using/writing-convex-functions for more.
5 |
6 | A query function that takes two arguments looks like:
7 |
8 | ```ts
9 | // functions.js
10 | import { query } from "./_generated/server";
11 | import { v } from "convex/values";
12 |
13 | export const myQueryFunction = query({
14 | // Validators for arguments.
15 | args: {
16 | first: v.number(),
17 | second: v.string(),
18 | },
19 |
20 | // Function implementation.
21 | hander: async (ctx, args) => {
22 | // Read the database as many times as you need here.
23 | // See https://docs.convex.dev/database/reading-data.
24 | const documents = await ctx.db.query("tablename").collect();
25 |
26 | // Arguments passed from the client are properties of the args object.
27 | console.log(args.first, args.second);
28 |
29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data,
30 | // remove non-public properties, or create new objects.
31 | return documents;
32 | },
33 | });
34 | ```
35 |
36 | Using this query function in a React component looks like:
37 |
38 | ```ts
39 | const data = useQuery(api.functions.myQueryFunction, {
40 | first: 10,
41 | second: "hello",
42 | });
43 | ```
44 |
45 | A mutation function looks like:
46 |
47 | ```ts
48 | // functions.js
49 | import { mutation } from "./_generated/server";
50 | import { v } from "convex/values";
51 |
52 | export const myMutationFunction = mutation({
53 | // Validators for arguments.
54 | args: {
55 | first: v.string(),
56 | second: v.string(),
57 | },
58 |
59 | // Function implementation.
60 | hander: async (ctx, args) => {
61 | // Insert or modify documents in the database here.
62 | // Mutations can also read from the database like queries.
63 | // See https://docs.convex.dev/database/writing-data.
64 | const message = { body: args.first, author: args.second };
65 | const id = await ctx.db.insert("messages", message);
66 |
67 | // Optionally, return a value from your mutation.
68 | return await ctx.db.get(id);
69 | },
70 | });
71 | ```
72 |
73 | Using this mutation function in a React component looks like:
74 |
75 | ```ts
76 | const mutation = useMutation(api.functions.myMutationFunction);
77 | function handleButtonPress() {
78 | // fire and forget, the most common way to use mutations
79 | mutation({ first: "Hello!", second: "me" });
80 | // OR
81 | // use the result once the mutation has completed
82 | mutation({ first: "Hello!", second: "me" }).then((result) =>
83 | console.log(result)
84 | );
85 | }
86 | ```
87 |
88 | Use the Convex CLI to push your functions to a deployment. See everything
89 | the Convex CLI can do by running `npx convex -h` in your project root
90 | directory. To learn more, launch the docs with `npx convex docs`.
91 |
--------------------------------------------------------------------------------
/convex/_generated/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated utilities for implementing server-side Convex query and mutation functions.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * Generated by convex@1.0.2.
8 | * To regenerate, run `npx convex dev`.
9 | * @module
10 | */
11 |
12 | import {
13 | actionGeneric,
14 | httpActionGeneric,
15 | queryGeneric,
16 | mutationGeneric,
17 | internalActionGeneric,
18 | internalMutationGeneric,
19 | internalQueryGeneric,
20 | } from "convex/server";
21 |
22 | /**
23 | * Define a query in this Convex app's public API.
24 | *
25 | * This function will be allowed to read your Convex database and will be accessible from the client.
26 | *
27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
29 | */
30 | export const query = queryGeneric;
31 |
32 | /**
33 | * Define a query that is only accessible from other Convex functions (but not from the client).
34 | *
35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client.
36 | *
37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
39 | */
40 | export const internalQuery = internalQueryGeneric;
41 |
42 | /**
43 | * Define a mutation in this Convex app's public API.
44 | *
45 | * This function will be allowed to modify your Convex database and will be accessible from the client.
46 | *
47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
49 | */
50 | export const mutation = mutationGeneric;
51 |
52 | /**
53 | * Define a mutation that is only accessible from other Convex functions (but not from the client).
54 | *
55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client.
56 | *
57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
59 | */
60 | export const internalMutation = internalMutationGeneric;
61 |
62 | /**
63 | * Define an action in this Convex app's public API.
64 | *
65 | * An action is a function which can execute any JavaScript code, including non-deterministic
66 | * code and code with side-effects, like calling third-party services.
67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
69 | *
70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument.
71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
72 | */
73 | export const action = actionGeneric;
74 |
75 | /**
76 | * Define an action that is only accessible from other Convex functions (but not from the client).
77 | *
78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument.
79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
80 | */
81 | export const internalAction = internalActionGeneric;
82 |
83 | /**
84 | * Define a Convex HTTP action.
85 | *
86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
87 | * as its second.
88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
89 | */
90 | export const httpAction = httpActionGeneric;
91 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/convex/withAuth.ts:
--------------------------------------------------------------------------------
1 | import { ObjectType, PropertyValidators, v } from "convex/values";
2 | import { Session } from "lucia";
3 | import {
4 | DatabaseWriter,
5 | MutationCtx,
6 | QueryCtx,
7 | internalMutation,
8 | internalQuery,
9 | mutation,
10 | query,
11 | } from "./_generated/server";
12 | import { Auth, getAuth } from "./lucia";
13 |
14 | export function queryWithAuth<
15 | ArgsValidator extends PropertyValidators,
16 | Output
17 | >({
18 | args,
19 | handler,
20 | }: {
21 | args: ArgsValidator;
22 | handler: (
23 | ctx: Omit & { session: Session | null },
24 | args: ObjectType
25 | ) => Output;
26 | }) {
27 | return query({
28 | args: {
29 | ...args,
30 | sessionId: v.union(v.null(), v.string()),
31 | },
32 | handler: async (ctx, args: any) => {
33 | const session = await getValidExistingSession(ctx, args.sessionId);
34 | return handler({ ...ctx, session }, args);
35 | },
36 | });
37 | }
38 |
39 | export function internalQueryWithAuth<
40 | ArgsValidator extends PropertyValidators,
41 | Output
42 | >({
43 | args,
44 | handler,
45 | }: {
46 | args: ArgsValidator;
47 | handler: (
48 | ctx: Omit & { session: Session | null },
49 | args: ObjectType
50 | ) => Output;
51 | }) {
52 | return internalQuery({
53 | args: { ...args, sessionId: v.union(v.null(), v.string()) },
54 | handler: async (ctx, args: any) => {
55 | const session = await getValidExistingSession(ctx, args.sessionId);
56 | return handler({ ...ctx, session }, args);
57 | },
58 | });
59 | }
60 |
61 | export function mutationWithAuth<
62 | ArgsValidator extends PropertyValidators,
63 | Output
64 | >({
65 | args,
66 | handler,
67 | }: {
68 | args: ArgsValidator;
69 | handler: (
70 | ctx: Omit & { auth: Auth; session: Session | null },
71 | args: ObjectType
72 | ) => Output;
73 | }) {
74 | return mutation({
75 | args: { ...args, sessionId: v.union(v.null(), v.string()) },
76 | handler: async (ctx, args: any) => {
77 | const auth = getAuth(ctx.db);
78 | const session = await getValidSessionAndRenew(auth, args.sessionId);
79 | return handler({ ...ctx, session, auth }, args);
80 | },
81 | });
82 | }
83 |
84 | export function internalMutationWithAuth<
85 | ArgsValidator extends PropertyValidators,
86 | Output
87 | >({
88 | args,
89 | handler,
90 | }: {
91 | args: ArgsValidator;
92 | handler: (
93 | ctx: Omit & { auth: Auth; session: Session | null },
94 | args: ObjectType
95 | ) => Output;
96 | }) {
97 | return internalMutation({
98 | args: { ...args, sessionId: v.union(v.null(), v.string()) },
99 | handler: async (ctx, args: any) => {
100 | const auth = getAuth(ctx.db);
101 | const session = await getValidSessionAndRenew(auth, args.sessionId);
102 | return handler({ ...ctx, session, auth }, args);
103 | },
104 | });
105 | }
106 |
107 | async function getValidExistingSession(
108 | ctx: QueryCtx,
109 | sessionId: string | null
110 | ) {
111 | if (sessionId === null) {
112 | return null;
113 | }
114 | // The cast is OK because we will only expose the existing session
115 | const auth = getAuth(ctx.db as DatabaseWriter);
116 | try {
117 | const session = (await auth.getSession(sessionId)) as Session | null;
118 | if (session === null || session.state === "idle") {
119 | return null;
120 | }
121 | return session;
122 | } catch (error) {
123 | // Invalid session ID
124 | return null;
125 | }
126 | }
127 |
128 | async function getValidSessionAndRenew(auth: Auth, sessionId: string | null) {
129 | if (sessionId === null) {
130 | return null;
131 | }
132 | try {
133 | return await auth.validateSession(sessionId);
134 | } catch (error) {
135 | // Invalid session ID
136 | return null;
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Convex Lucia Auth Demo
2 |
3 | This example app showcases authentication built entirely on top of Convex without any third-party platform. It uses [Lucia](https://lucia-auth.com/) for the authentication logic.
4 |
5 | 
6 |
7 | Features:
8 |
9 | - Without any additional setup, you can sign in with an email+password combination
10 | - Sign out button
11 | - Session is preserved in `localStorage`
12 | - Passwords are securely hashed
13 |
14 | This integration works! You can see a production deployment at this live site: https://get-convex.github.io/convex-lucia-auth-demo/.
15 |
16 | Read a deep dive into this codebase at [Custom Authentication (with Lucia)](https://stack.convex.dev/convex-with-lucia) on Stack.
17 |
18 | ## Setting up
19 |
20 | Run:
21 |
22 | ```
23 | npm install
24 | npm run dev
25 | ```
26 |
27 | This will guide you through making a Convex project, and then open the web browser with the app running.
28 |
29 | ## Deploying to production
30 |
31 | Follow these steps to deploy this repo to production:
32 |
33 | 1. Configure `VITE_CONVEX_URL` either in your hosting provider or in a `.env.production` file
34 | 2. In your production deployment's settings page configure these variables:
35 |
36 | - `LUCIA_ENVIRONMENT`=`PROD`
37 | - `HOSTNAME`=where the app is hosted, such as `foo.github.io` or `mydomain.com`
38 |
39 | ## Note on CSRF protection
40 |
41 | This demo uses `localStorage` for storing the secret `sessionId`. This means that sessions are only preserved on pages served on the same subdomain, such as `foo.example.com` or `username.github.io`. This prevents CSRF attacks.
42 |
43 | This does though invite an [XSS attack](https://en.wikipedia.org/wiki/Cross-site_scripting). Make sure your app is not susceptable to XSS.
44 |
45 | Convex currently doesn't support accessing cookies in queries and mutations, so cookie-based authentication can only be used in Convex HTTP actions (not demonstrated in this demo).
46 |
47 | # What is Convex?
48 |
49 | [Convex](https://convex.dev) is a hosted backend platform with a
50 | built-in database that lets you write your
51 | [database schema](https://docs.convex.dev/database/schemas) and
52 | [server functions](https://docs.convex.dev/functions) in
53 | [TypeScript](https://docs.convex.dev/typescript). Server-side database
54 | [queries](https://docs.convex.dev/functions/query-functions) automatically
55 | [cache](https://docs.convex.dev/functions/query-functions#caching--reactivity) and
56 | [subscribe](https://docs.convex.dev/client/react#reactivity) to data, powering a
57 | [realtime `useQuery` hook](https://docs.convex.dev/client/react#fetching-data) in our
58 | [React client](https://docs.convex.dev/client/react). There are also
59 | [Python](https://docs.convex.dev/client/python),
60 | [Rust](https://docs.convex.dev/client/rust),
61 | [ReactNative](https://docs.convex.dev/client/react-native), and
62 | [Node](https://docs.convex.dev/client/javascript) clients, as well as a straightforward
63 | [HTTP API](https://github.com/get-convex/convex-js/blob/main/src/browser/http_client.ts#L40).
64 |
65 | The database support
66 | [NoSQL-style documents](https://docs.convex.dev/database/document-storage) with
67 | [relationships](https://docs.convex.dev/database/document-ids) and
68 | [custom indexes](https://docs.convex.dev/database/indexes/)
69 | (including on fields in nested objects).
70 |
71 | The
72 | [`query`](https://docs.convex.dev/functions/query-functions) and
73 | [`mutation`](https://docs.convex.dev/functions/mutation-functions) server functions have transactional,
74 | low latency access to the database and leverage our
75 | [`v8` runtime](https://docs.convex.dev/functions/runtimes) with
76 | [determinism guardrails](https://docs.convex.dev/functions/runtimes#using-randomness-and-time-in-queries-and-mutations)
77 | to provide the strongest ACID guarantees on the market:
78 | immediate consistency,
79 | serializable isolation, and
80 | automatic conflict resolution via
81 | [optimistic multi-version concurrency control](https://docs.convex.dev/database/advanced/occ) (OCC / MVCC).
82 |
83 | The [`action` server functions](https://docs.convex.dev/functions/actions) have
84 | access to external APIs and enable other side-effects and non-determinism in
85 | either our
86 | [optimized `v8` runtime](https://docs.convex.dev/functions/runtimes) or a more
87 | [flexible `node` runtime](https://docs.convex.dev/functions/runtimes#nodejs-runtime).
88 |
89 | Functions can run in the background via
90 | [scheduling](https://docs.convex.dev/scheduling/scheduled-functions) and
91 | [cron jobs](https://docs.convex.dev/scheduling/cron-jobs).
92 |
93 | Development is cloud-first, with
94 | [hot reloads for server function](https://docs.convex.dev/cli#run-the-convex-dev-server) editing via the
95 | [CLI](https://docs.convex.dev/cli). There is a
96 | [dashbord UI](https://docs.convex.dev/dashboard) to
97 | [browse and edit data](https://docs.convex.dev/dashboard/deployments/data),
98 | [edit environment variables](https://docs.convex.dev/production/environment-variables),
99 | [view logs](https://docs.convex.dev/dashboard/deployments/logs),
100 | [run server functions](https://docs.convex.dev/dashboard/deployments/functions), and more.
101 |
102 | There are built-in features for
103 | [reactive pagination](https://docs.convex.dev/database/pagination),
104 | [file storage](https://docs.convex.dev/file-storage),
105 | [reactive search](https://docs.convex.dev/text-search),
106 | [https endpoints](https://docs.convex.dev/functions/http-actions) (for webhooks),
107 | [streaming import/export](https://docs.convex.dev/database/import-export/), and
108 | [runtime data validation](https://docs.convex.dev/database/schemas#validators) for
109 | [function arguments](https://docs.convex.dev/functions/args-validation) and
110 | [database data](https://docs.convex.dev/database/schemas#schema-validation).
111 |
112 | Everything scales automatically, and it’s [free to start](https://www.convex.dev/plans).
113 |
--------------------------------------------------------------------------------
/convex/lucia.ts:
--------------------------------------------------------------------------------
1 | // lucia.ts
2 | import {
3 | Adapter,
4 | KeySchema,
5 | LuciaErrorConstructor,
6 | SessionSchema,
7 | UserSchema,
8 | lucia,
9 | } from "lucia";
10 | import { DatabaseReader, DatabaseWriter } from "./_generated/server";
11 |
12 | type SessionId = string;
13 | type UserId = string;
14 | type KeyId = string;
15 |
16 | export function getAuth(db: DatabaseWriter) {
17 | return lucia({
18 | // We cheat to allow queries to use `getAuth`
19 | adapter: convexAdapter(db),
20 | // TODO: Set the LUCIA_ENVIRONMENT variable to "PROD"
21 | // on your prod deployment's dashboard
22 | env: (process.env.LUCIA_ENVIRONMENT as "PROD" | undefined) ?? "DEV",
23 | getUserAttributes(user: UserSchema) {
24 | return {
25 | _id: user._id,
26 | _creationTime: user._creationTime,
27 | email: user.email,
28 | };
29 | },
30 | });
31 | }
32 |
33 | const convexAdapter = (db: DatabaseWriter) => {
34 | return (LuciaError: LuciaErrorConstructor): Adapter => ({
35 | async getSessionAndUser(
36 | sessionId: string
37 | ): Promise<[SessionSchema, UserSchema] | [null, null]> {
38 | const session = await getSession(db, sessionId);
39 | if (session === null) {
40 | return [null, null];
41 | }
42 | const user = await getUser(db, session.user_id);
43 | if (user === null) {
44 | return [null, null];
45 | }
46 | return [session, user];
47 | },
48 | async deleteSession(sessionId: SessionId): Promise {
49 | const session = await getSession(db, sessionId);
50 | if (session === null) {
51 | return;
52 | }
53 | await db.delete(session._id);
54 | },
55 | async deleteSessionsByUserId(userId: UserId): Promise {
56 | const sessions = await this.getSessionsByUserId(userId);
57 | await Promise.all(sessions.map((session) => db.delete(session._id)));
58 | },
59 | async getSession(sessionId: SessionId): Promise {
60 | return await getSession(db, sessionId);
61 | },
62 | async getSessionsByUserId(userId: UserId): Promise {
63 | return await db
64 | .query("sessions")
65 | .withIndex("byUserId", (q) => q.eq("user_id", userId))
66 | .collect();
67 | },
68 | async setSession(session: SessionSchema): Promise {
69 | const { _id, _creationTime, ...data } = session;
70 | await db.insert("sessions", data);
71 | },
72 | async deleteKeysByUserId(userId: UserId): Promise {
73 | const keys = await db
74 | .query("auth_keys")
75 | .withIndex("byUserId", (q) => q.eq("user_id", userId))
76 | .collect();
77 | await Promise.all(keys.map((key) => db.delete(key._id)));
78 | },
79 | async deleteKey(keyId: KeyId): Promise {
80 | const key = await getKey(db, keyId);
81 | if (key === null) {
82 | return;
83 | }
84 | await db.delete(key._id);
85 | },
86 | async deleteUser(userId: UserId): Promise {
87 | const user = await getUser(db, userId);
88 | if (user === null) {
89 | return;
90 | }
91 | await db.delete(user._id);
92 | },
93 | async getKey(keyId: KeyId): Promise {
94 | return await getKey(db, keyId);
95 | },
96 | async getKeysByUserId(userId: UserId): Promise {
97 | return await db
98 | .query("auth_keys")
99 | .withIndex("byUserId", (q) => q.eq("user_id", userId))
100 | .collect();
101 | },
102 | async getUser(userId: UserId): Promise {
103 | return await getUser(db, userId);
104 | },
105 | async setKey(key: KeySchema): Promise {
106 | const existingKey = await this.getKey(key.id);
107 | if (existingKey !== null) {
108 | throw new LuciaError("AUTH_DUPLICATE_KEY_ID");
109 | }
110 | const user = await this.getUser(key.user_id);
111 | if (user === null) {
112 | throw new LuciaError("AUTH_INVALID_USER_ID");
113 | }
114 | await db.insert("auth_keys", key);
115 | },
116 | async setUser(user: UserSchema, key: KeySchema | null): Promise {
117 | const { _id, _creationTime, ...data } = user;
118 | await db.insert("users", data);
119 | if (key !== null) {
120 | await this.setKey(key);
121 | }
122 | },
123 | async updateKey(
124 | keyId: string,
125 | partialKey: Partial
126 | ): Promise {
127 | const key = await getKey(db, keyId);
128 | if (key === null) {
129 | throw new LuciaError("AUTH_INVALID_KEY_ID");
130 | }
131 | await db.patch(key._id, partialKey);
132 | },
133 | async updateUser(
134 | userId: string,
135 | partialUser: Partial
136 | ): Promise {
137 | const user = await getUser(db, userId);
138 | if (user === null) {
139 | throw new LuciaError("AUTH_INVALID_USER_ID");
140 | }
141 | await db.patch(user._id, partialUser);
142 | },
143 | async updateSession(
144 | sessionId: string,
145 | partialSession: Partial
146 | ): Promise {
147 | const session = await getSession(db, sessionId);
148 | if (session === null) {
149 | throw new LuciaError("AUTH_INVALID_SESSION_ID");
150 | }
151 | await db.patch(session._id, partialSession);
152 | },
153 | });
154 | };
155 |
156 | async function getSession(db: DatabaseReader, sessionId: string) {
157 | return await db
158 | .query("sessions")
159 | .withIndex("byId", (q) => q.eq("id", sessionId))
160 | .first();
161 | }
162 |
163 | async function getUser(db: DatabaseReader, userId: string) {
164 | return await db
165 | .query("users")
166 | .withIndex("byId", (q) => q.eq("id", userId))
167 | .first();
168 | }
169 |
170 | async function getKey(db: DatabaseReader, keyId: string) {
171 | return await db
172 | .query("auth_keys")
173 | .withIndex("byId", (q) => q.eq("id", keyId))
174 | .first();
175 | }
176 |
177 | export type Auth = ReturnType;
178 |
--------------------------------------------------------------------------------
/convex/_generated/server.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /**
3 | * Generated utilities for implementing server-side Convex query and mutation functions.
4 | *
5 | * THIS CODE IS AUTOMATICALLY GENERATED.
6 | *
7 | * Generated by convex@1.0.2.
8 | * To regenerate, run `npx convex dev`.
9 | * @module
10 | */
11 |
12 | import {
13 | ActionBuilder,
14 | HttpActionBuilder,
15 | MutationBuilder,
16 | QueryBuilder,
17 | ActionCtx as GenericActionCtx,
18 | MutationCtx as GenericMutationCtx,
19 | QueryCtx as GenericQueryCtx,
20 | DatabaseReader as GenericDatabaseReader,
21 | DatabaseWriter as GenericDatabaseWriter,
22 | } from "convex/server";
23 | import type { DataModel } from "./dataModel.js";
24 |
25 | /**
26 | * Define a query in this Convex app's public API.
27 | *
28 | * This function will be allowed to read your Convex database and will be accessible from the client.
29 | *
30 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
31 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
32 | */
33 | export declare const query: QueryBuilder;
34 |
35 | /**
36 | * Define a query that is only accessible from other Convex functions (but not from the client).
37 | *
38 | * This function will be allowed to read from your Convex database. It will not be accessible from the client.
39 | *
40 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
41 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
42 | */
43 | export declare const internalQuery: QueryBuilder;
44 |
45 | /**
46 | * Define a mutation in this Convex app's public API.
47 | *
48 | * This function will be allowed to modify your Convex database and will be accessible from the client.
49 | *
50 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
51 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
52 | */
53 | export declare const mutation: MutationBuilder;
54 |
55 | /**
56 | * Define a mutation that is only accessible from other Convex functions (but not from the client).
57 | *
58 | * This function will be allowed to modify your Convex database. It will not be accessible from the client.
59 | *
60 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
61 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
62 | */
63 | export declare const internalMutation: MutationBuilder;
64 |
65 | /**
66 | * Define an action in this Convex app's public API.
67 | *
68 | * An action is a function which can execute any JavaScript code, including non-deterministic
69 | * code and code with side-effects, like calling third-party services.
70 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
71 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
72 | *
73 | * @param func - The action. It receives an {@link ActionCtx} as its first argument.
74 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
75 | */
76 | export declare const action: ActionBuilder<"public">;
77 |
78 | /**
79 | * Define an action that is only accessible from other Convex functions (but not from the client).
80 | *
81 | * @param func - The function. It receives an {@link ActionCtx} as its first argument.
82 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
83 | */
84 | export declare const internalAction: ActionBuilder<"internal">;
85 |
86 | /**
87 | * Define an HTTP action.
88 | *
89 | * This function will be used to respond to HTTP requests received by a Convex
90 | * deployment if the requests matches the path and method where this action
91 | * is routed. Be sure to route your action in `convex/http.js`.
92 | *
93 | * @param func - The function. It receives an {@link ActionCtx} as its first argument.
94 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
95 | */
96 | export declare const httpAction: HttpActionBuilder;
97 |
98 | /**
99 | * A set of services for use within Convex query functions.
100 | *
101 | * The query context is passed as the first argument to any Convex query
102 | * function run on the server.
103 | *
104 | * This differs from the {@link MutationCtx} because all of the services are
105 | * read-only.
106 | */
107 | export type QueryCtx = GenericQueryCtx;
108 |
109 | /**
110 | * A set of services for use within Convex mutation functions.
111 | *
112 | * The mutation context is passed as the first argument to any Convex mutation
113 | * function run on the server.
114 | */
115 | export type MutationCtx = GenericMutationCtx;
116 |
117 | /**
118 | * A set of services for use within Convex action functions.
119 | *
120 | * The action context is passed as the first argument to any Convex action
121 | * function run on the server.
122 | */
123 | export type ActionCtx = GenericActionCtx;
124 |
125 | /**
126 | * An interface to read from the database within Convex query functions.
127 | *
128 | * The two entry points are {@link DatabaseReader.get}, which fetches a single
129 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts
130 | * building a query.
131 | */
132 | export type DatabaseReader = GenericDatabaseReader;
133 |
134 | /**
135 | * An interface to read from and write to the database within Convex mutation
136 | * functions.
137 | *
138 | * Convex guarantees that all writes within a single mutation are
139 | * executed atomically, so you never have to worry about partial writes leaving
140 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
141 | * for the guarantees Convex provides your functions.
142 | */
143 | export type DatabaseWriter = GenericDatabaseWriter;
144 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2024 Convex, Inc.
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------