├── cjs ├── package.json ├── react.js.map └── react.js ├── bun.lockb ├── env.d.ts ├── .gitignore ├── .tsconfig.esm.json ├── tsconfig.json ├── .tsconfig.cjs.json ├── package.json ├── src ├── email.ts ├── react.tsx └── index.ts ├── esm ├── react.js.map └── react.js ├── README.md ├── DOCS.md └── LICENSE /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type": "commonjs"} 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/convex-lucia-auth/main/bun.lockb -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Lucia { 2 | type Auth = import("./src/index").Auth; 3 | type DatabaseUserAttributes = import("./src/index").DatabaseUserAttributes; 4 | type DatabaseSessionAttributes = 5 | import("./src/index").DatabaseSessionAttributes; 6 | } 7 | 8 | declare namespace ConvexLuciaAuth { 9 | type DataModel = import("./src/index").MinimalDataModel; 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env.production 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /.tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "outDir": "esm", 5 | "sourceMap": true, 6 | "lib": ["ES2021", "dom"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "isolatedModules": true, 13 | "jsx": "react-jsx", 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["./src/react.tsx"] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "noEmit": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["src", "env.d.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /.tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "cjs", 5 | "sourceMap": true, 6 | "lib": ["ES2021", "dom"], 7 | "module": "commonjs", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "node", 12 | "isolatedModules": true, 13 | "jsx": "react-jsx", 14 | "allowSyntheticDefaultImports": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["./src/react.tsx"] 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@convex-dev/convex-lucia-auth", 3 | "version": "0.0.5", 4 | "description": "Convex database adapter for Lucia Auth", 5 | "homepage": "https://convex.dev/", 6 | "repository": "https://github.com/get-convex/convex-lucia-auth", 7 | "keywords": [ 8 | "auth", 9 | "lucia", 10 | "clerk", 11 | "authentication", 12 | "db", 13 | "database", 14 | "react" 15 | ], 16 | "license": "Apache-2.0", 17 | "type": "module", 18 | "files": [ 19 | "src", 20 | "cjs", 21 | "esm", 22 | "README.md" 23 | ], 24 | "exports": { 25 | ".": { 26 | "types": "./src/index.ts", 27 | "default": "./src/index.ts" 28 | }, 29 | "./email": { 30 | "types": "./src/email.ts", 31 | "default": "./src/email.ts" 32 | }, 33 | "./react": { 34 | "types": "./src/react.tsx", 35 | "import": "./esm/react.js", 36 | "require": "./cjs/react.js" 37 | } 38 | }, 39 | "typesVersions": { 40 | "*": { 41 | "*": [ 42 | "./src/index.ts" 43 | ], 44 | "email": [ 45 | "./src/email.ts" 46 | ], 47 | "react": [ 48 | "./src/react.tsx" 49 | ] 50 | } 51 | }, 52 | "scripts": { 53 | "build": "rm -rf cjs esm && tsc -p .tsconfig.cjs.json && tsc -p .tsconfig.esm.json && echo '{\"type\": \"commonjs\"}' > cjs/package.json" 54 | }, 55 | "dependencies": { 56 | "lucia": "^2.6.0" 57 | }, 58 | "peerDependencies": { 59 | "convex": "^1.1.1", 60 | "react": "^18.2.0", 61 | "react-dom": "^18.2.0" 62 | }, 63 | "devDependencies": { 64 | "@types/node": "^20.4.6", 65 | "@types/react": "^18.2.15", 66 | "@types/react-dom": "^18.2.7", 67 | "@typescript-eslint/eslint-plugin": "^6.0.0", 68 | "@typescript-eslint/parser": "^6.0.0", 69 | "@vitejs/plugin-react": "^4.0.3", 70 | "eslint": "^8.45.0", 71 | "eslint-plugin-react-hooks": "^4.6.0", 72 | "eslint-plugin-react-refresh": "^0.4.3", 73 | "npm-run-all": "^4.1.5", 74 | "typescript": "^5.0.2", 75 | "vite": "^4.4.5" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/email.ts: -------------------------------------------------------------------------------- 1 | import { GenericId } from "convex/values"; 2 | import { DocumentByName } from "convex/server"; 3 | import { Auth } from "."; 4 | 5 | type EmptyObject = Record; 6 | 7 | type CustomUserFields = Omit< 8 | DocumentByName, 9 | "_id" | "_creationTime" | "id" | "email" 10 | >; 11 | type CustomSessionFields = Omit< 12 | DocumentByName, 13 | "_id" | "_creationTime" | "id" | "user_id" | "active_expires" | "idle_expires" 14 | >; 15 | 16 | export async function signInWithEmailAndPassword( 17 | ctx: { auth: Auth }, 18 | email: string, 19 | password: string, 20 | ...additionalFields: CustomSessionFields extends EmptyObject 21 | ? [additionalFields?: { session?: EmptyObject }] 22 | : [additionalFields: { session: CustomSessionFields }] 23 | ) { 24 | const key = await ctx.auth.useKey("password", email, password); 25 | const session = await ctx.auth.createSession({ 26 | userId: key.userId, 27 | attributes: { 28 | // These will be filled out by Convex 29 | _id: "" as GenericId<"sessions">, 30 | _creationTime: 0, 31 | ...additionalFields[0]?.session, 32 | }, 33 | }); 34 | return session; 35 | } 36 | 37 | export async function signUpWithEmailAndPassword( 38 | ctx: { auth: Auth }, 39 | email: string, 40 | password: string, 41 | ...additionalFields: 42 | | CustomUserFields 43 | | CustomSessionFields extends EmptyObject 44 | ? [ 45 | additionalFields?: { 46 | user?: EmptyObject; 47 | session?: EmptyObject; 48 | } 49 | ] 50 | : [ 51 | additionalFields: { 52 | user: CustomUserFields; 53 | session: CustomSessionFields; 54 | } 55 | ] 56 | ) { 57 | const user = await ctx.auth.createUser({ 58 | key: { 59 | password: password, 60 | providerId: "password", 61 | providerUserId: email, 62 | }, 63 | attributes: { 64 | // @ts-ignore Consumers of email should have it in their schema 65 | email, 66 | // These will be filled out by Convex 67 | _id: "" as GenericId<"users">, 68 | _creationTime: 0, 69 | ...additionalFields[0]?.user, 70 | }, 71 | }); 72 | const session = await ctx.auth.createSession({ 73 | userId: user.userId, 74 | attributes: { 75 | // These will be filled out by Convex 76 | _id: "" as GenericId<"sessions">, 77 | _creationTime: 0, 78 | ...additionalFields[0]?.session, 79 | }, 80 | }); 81 | return session; 82 | } 83 | -------------------------------------------------------------------------------- /esm/react.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /cjs/react.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /esm/react.js: -------------------------------------------------------------------------------- 1 | import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; 2 | import { useMutation, useQuery } from "convex/react"; 3 | import { createContext, useCallback, useContext, useState, } from "react"; 4 | const SessionContext = createContext(undefined); 5 | export function useSessionId() { 6 | return useContext(SessionContext).sessionId; 7 | } 8 | export function useSetSessionId() { 9 | return useContext(SessionContext).setSessionId; 10 | } 11 | export function SessionProvider({ children }) { 12 | const [sessionId, setSessionIdState] = useState(getSavedSessionId()); 13 | const setSessionId = useCallback((value) => { 14 | setSavedSessionId(value); 15 | setSessionIdState(value); 16 | }, [setSessionIdState]); 17 | return (_jsx(SessionContext.Provider, { value: { sessionId, setSessionId }, children: children })); 18 | } 19 | function getSavedSessionId() { 20 | return localStorage.getItem("sessionId"); 21 | } 22 | export function setSavedSessionId(sessionId) { 23 | if (sessionId == null) { 24 | localStorage.removeItem("sessionId"); 25 | } 26 | else { 27 | localStorage.setItem("sessionId", sessionId); 28 | } 29 | } 30 | // Sign in / sign up 31 | export function SignUpSignIn({ labelClassName, inputClassName, buttonClassName, flowToggleClassName, errorDisplayClassName, signIn, signUp, onError, }) { 32 | const { flow, toggleFlow, onSubmit, error } = useSignUpSignIn({ 33 | signIn, 34 | signUp, 35 | onError, 36 | }); 37 | return (_jsxs("form", { onSubmit: onSubmit, children: [_jsx("label", { className: labelClassName, htmlFor: "username", children: "Email" }), _jsx("input", { className: inputClassName, name: "email", id: "email", autoComplete: "username" }), _jsx("label", { className: labelClassName, htmlFor: "password", children: "Password" }), _jsx("input", { className: inputClassName, type: "password", name: "password", id: "password", autoComplete: flow === "signIn" ? "current-password" : "new-password" }), _jsx("input", { className: buttonClassName, type: "submit", value: flow === "signIn" ? "Sign in" : "Sign up" }), _jsx("a", { className: flowToggleClassName, onClick: toggleFlow, children: flow === "signIn" 38 | ? "Don't have an account? Sign up" 39 | : "Already have an account? Sign in" }), _jsx("div", { className: errorDisplayClassName, children: error !== undefined 40 | ? flow === "signIn" 41 | ? "Could not sign in, did you mean to sign up?" 42 | : "Could not sign up, did you mean to sign in?" 43 | : null })] })); 44 | } 45 | // Sign out 46 | export function SignOutButton({ className }) { 47 | return (_jsx("button", { className: className, onClick: useSignOut(), children: "Logout" })); 48 | } 49 | // Hooks 50 | export function useSignUpSignIn({ signIn, signUp, onError, }) { 51 | const setSessionId = useSetSessionId(); 52 | const [flow, setFlow] = useState("signIn"); 53 | const [error, setError] = useState(); 54 | const toggleFlow = useCallback(() => { 55 | setFlow(flow === "signIn" ? "signUp" : "signIn"); 56 | clearError(); 57 | }, [flow, setFlow]); 58 | const clearError = useCallback(() => setError(undefined), [setError]); 59 | const onSubmit = async (event) => { 60 | event.preventDefault(); 61 | clearError(); 62 | const data = new FormData(event.currentTarget); 63 | try { 64 | const sessionId = await (flow === "signIn" ? signIn : signUp)({ 65 | email: data.get("email") ?? "", 66 | password: data.get("password") ?? "", 67 | }); 68 | setSessionId(sessionId); 69 | } 70 | catch (error) { 71 | onError?.(flow, error); 72 | setError(error); 73 | } 74 | }; 75 | return { onSubmit, flow, setFlow, toggleFlow, error, setError, clearError }; 76 | } 77 | export function useSignOut() { 78 | const setSessionId = useSetSessionId(); 79 | return useCallback(() => setSessionId(null), [setSessionId]); 80 | } 81 | // Convex Hooks 82 | export function useQueryWithAuth(query, args) { 83 | const sessionId = useSessionId(); 84 | return useQuery(query, { ...args, sessionId }); 85 | } 86 | export function useMutationWithAuth(mutation) { 87 | const doMutation = useMutation(mutation); 88 | const sessionId = useSessionId(); 89 | return useCallback((args) => { 90 | return doMutation({ ...args, sessionId }); 91 | }, [doMutation, sessionId]); // We don't support optimistic updates 92 | } 93 | //# sourceMappingURL=react.js.map -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | This library uses Lucia 2.x, which has since been deprecated. We recommend using [Convex Auth](https://docs.convex.dev/auth/convex-auth) for self-hosting auth on Convex or [Clerk](https://docs.convex.dev/auth/clerk) if you prefer a fully managed authentication service. 3 | 4 | # Convex Database Adapter + UI for Lucia Auth Library 5 | 6 | This library enables authentication built entirely on top of Convex without any third-party platform. It uses [Lucia](https://lucia-auth.com/) for the authentication logic. 7 | 8 | ![Screenshot of the app](https://github.com/get-convex/convex-lucia-auth-demo/raw/main/screenshot.png) 9 | 10 | Features: 11 | 12 | - Without any additional setup, you can sign in with an email+password combination 13 | - Sign out button 14 | - Session is preserved in `localStorage` 15 | - Passwords are securely hashed 16 | 17 | This integration works! You can see a production deployment at this live site: https://get-convex.github.io/convex-lucia-auth-demo/. 18 | 19 | ## Install 20 | 21 | ``` 22 | npm install @convex-dev/convex-lucia-auth 23 | ``` 24 | 25 | Checkout [Docs](https://github.com/get-convex/convex-lucia-auth/blob/master/DOCS.md) for how to integrate the library into your app. 26 | 27 | ## Deploying to production 28 | 29 | In your production deployment's settings page configure this variable: 30 | 31 | - `LUCIA_ENVIRONMENT`=`PROD` 32 | 33 | ## Note on CSRF protection 34 | 35 | The React components use `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. 36 | 37 | This does though invite an [XSS attack](https://en.wikipedia.org/wiki/Cross-site_scripting). Make sure your app is not susceptable to XSS. 38 | 39 | Convex currently doesn't support accessing cookies in queries and mutations, so cookie-based authentication can only be used in Convex HTTP actions. 40 | 41 | # What is Convex? 42 | 43 | [Convex](https://convex.dev) is a hosted backend platform with a 44 | built-in database that lets you write your 45 | [database schema](https://docs.convex.dev/database/schemas) and 46 | [server functions](https://docs.convex.dev/functions) in 47 | [TypeScript](https://docs.convex.dev/typescript). Server-side database 48 | [queries](https://docs.convex.dev/functions/query-functions) automatically 49 | [cache](https://docs.convex.dev/functions/query-functions#caching--reactivity) and 50 | [subscribe](https://docs.convex.dev/client/react#reactivity) to data, powering a 51 | [realtime `useQuery` hook](https://docs.convex.dev/client/react#fetching-data) in our 52 | [React client](https://docs.convex.dev/client/react). There are also 53 | [Python](https://docs.convex.dev/client/python), 54 | [Rust](https://docs.convex.dev/client/rust), 55 | [ReactNative](https://docs.convex.dev/client/react-native), and 56 | [Node](https://docs.convex.dev/client/javascript) clients, as well as a straightforward 57 | [HTTP API](https://github.com/get-convex/convex-js/blob/main/src/browser/http_client.ts#L40). 58 | 59 | The database support 60 | [NoSQL-style documents](https://docs.convex.dev/database/document-storage) with 61 | [relationships](https://docs.convex.dev/database/document-ids) and 62 | [custom indexes](https://docs.convex.dev/database/indexes/) 63 | (including on fields in nested objects). 64 | 65 | The 66 | [`query`](https://docs.convex.dev/functions/query-functions) and 67 | [`mutation`](https://docs.convex.dev/functions/mutation-functions) server functions have transactional, 68 | low latency access to the database and leverage our 69 | [`v8` runtime](https://docs.convex.dev/functions/runtimes) with 70 | [determinism guardrails](https://docs.convex.dev/functions/runtimes#using-randomness-and-time-in-queries-and-mutations) 71 | to provide the strongest ACID guarantees on the market: 72 | immediate consistency, 73 | serializable isolation, and 74 | automatic conflict resolution via 75 | [optimistic multi-version concurrency control](https://docs.convex.dev/database/advanced/occ) (OCC / MVCC). 76 | 77 | The [`action` server functions](https://docs.convex.dev/functions/actions) have 78 | access to external APIs and enable other side-effects and non-determinism in 79 | either our 80 | [optimized `v8` runtime](https://docs.convex.dev/functions/runtimes) or a more 81 | [flexible `node` runtime](https://docs.convex.dev/functions/runtimes#nodejs-runtime). 82 | 83 | Functions can run in the background via 84 | [scheduling](https://docs.convex.dev/scheduling/scheduled-functions) and 85 | [cron jobs](https://docs.convex.dev/scheduling/cron-jobs). 86 | 87 | Development is cloud-first, with 88 | [hot reloads for server function](https://docs.convex.dev/cli#run-the-convex-dev-server) editing via the 89 | [CLI](https://docs.convex.dev/cli). There is a 90 | [dashbord UI](https://docs.convex.dev/dashboard) to 91 | [browse and edit data](https://docs.convex.dev/dashboard/deployments/data), 92 | [edit environment variables](https://docs.convex.dev/production/environment-variables), 93 | [view logs](https://docs.convex.dev/dashboard/deployments/logs), 94 | [run server functions](https://docs.convex.dev/dashboard/deployments/functions), and more. 95 | 96 | There are built-in features for 97 | [reactive pagination](https://docs.convex.dev/database/pagination), 98 | [file storage](https://docs.convex.dev/file-storage), 99 | [reactive search](https://docs.convex.dev/text-search), 100 | [https endpoints](https://docs.convex.dev/functions/http-actions) (for webhooks), 101 | [streaming import/export](https://docs.convex.dev/database/import-export/), and 102 | [runtime data validation](https://docs.convex.dev/database/schemas#validators) for 103 | [function arguments](https://docs.convex.dev/functions/args-validation) and 104 | [database data](https://docs.convex.dev/database/schemas#schema-validation). 105 | 106 | Everything scales automatically, and it’s [free to start](https://www.convex.dev/plans). 107 | -------------------------------------------------------------------------------- /src/react.tsx: -------------------------------------------------------------------------------- 1 | import { ReactMutation, useMutation, useQuery } from "convex/react"; 2 | import { FunctionReference } from "convex/server"; 3 | import { 4 | FormEvent, 5 | ReactNode, 6 | createContext, 7 | useCallback, 8 | useContext, 9 | useState, 10 | } from "react"; 11 | 12 | type SessionId = string; 13 | 14 | const SessionContext = createContext<{ 15 | sessionId: SessionId | null; 16 | setSessionId: (sessionId: SessionId | null) => void; 17 | }>(undefined as any); 18 | 19 | export function useSessionId() { 20 | return useContext(SessionContext)!.sessionId; 21 | } 22 | 23 | export function useSetSessionId() { 24 | return useContext(SessionContext!)!.setSessionId; 25 | } 26 | 27 | export function SessionProvider({ children }: { children: ReactNode }) { 28 | const [sessionId, setSessionIdState] = useState(getSavedSessionId()); 29 | const setSessionId = useCallback( 30 | (value: SessionId | null) => { 31 | setSavedSessionId(value); 32 | setSessionIdState(value); 33 | }, 34 | [setSessionIdState] 35 | ); 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | } 42 | 43 | function getSavedSessionId() { 44 | return localStorage.getItem("sessionId"); 45 | } 46 | 47 | export function setSavedSessionId(sessionId: SessionId | null) { 48 | if (sessionId == null) { 49 | localStorage.removeItem("sessionId"); 50 | } else { 51 | localStorage.setItem("sessionId", sessionId); 52 | } 53 | } 54 | 55 | // Sign in / sign up 56 | export function SignUpSignIn({ 57 | labelClassName, 58 | inputClassName, 59 | buttonClassName, 60 | flowToggleClassName, 61 | errorDisplayClassName, 62 | signIn, 63 | signUp, 64 | onError, 65 | }: { 66 | labelClassName?: string; 67 | inputClassName?: string; 68 | buttonClassName?: string; 69 | flowToggleClassName?: string; 70 | errorDisplayClassName?: string; 71 | signIn: (args: { email: string; password: string }) => Promise; 72 | signUp: (args: { email: string; password: string }) => Promise; 73 | onError?: (flow: "signIn" | "signUp", error: unknown) => void; 74 | }) { 75 | const { flow, toggleFlow, onSubmit, error } = useSignUpSignIn({ 76 | signIn, 77 | signUp, 78 | onError, 79 | }); 80 | return ( 81 |
82 | 85 | 91 | 94 | 101 | 106 | 107 | {flow === "signIn" 108 | ? "Don't have an account? Sign up" 109 | : "Already have an account? Sign in"} 110 | 111 |
112 | {error !== undefined 113 | ? flow === "signIn" 114 | ? "Could not sign in, did you mean to sign up?" 115 | : "Could not sign up, did you mean to sign in?" 116 | : null} 117 |
118 |
119 | ); 120 | } 121 | 122 | // Sign out 123 | export function SignOutButton({ className }: { className?: string }) { 124 | return ( 125 | 128 | ); 129 | } 130 | 131 | // Hooks 132 | 133 | export function useSignUpSignIn({ 134 | signIn, 135 | signUp, 136 | onError, 137 | }: { 138 | signIn: (args: { email: string; password: string }) => Promise; 139 | signUp: (args: { email: string; password: string }) => Promise; 140 | onError?: (flow: "signIn" | "signUp", error: unknown) => void; 141 | }) { 142 | const setSessionId = useSetSessionId(); 143 | const [flow, setFlow] = useState<"signIn" | "signUp">("signIn"); 144 | const [error, setError] = useState(); 145 | const toggleFlow = useCallback(() => { 146 | setFlow(flow === "signIn" ? "signUp" : "signIn"); 147 | clearError(); 148 | }, [flow, setFlow]); 149 | const clearError = useCallback(() => setError(undefined), [setError]); 150 | const onSubmit = async (event: FormEvent) => { 151 | event.preventDefault(); 152 | clearError(); 153 | const data = new FormData(event.currentTarget); 154 | try { 155 | const sessionId = await (flow === "signIn" ? signIn : signUp)({ 156 | email: (data.get("email") as string | null) ?? "", 157 | password: (data.get("password") as string | null) ?? "", 158 | }); 159 | setSessionId(sessionId); 160 | } catch (error) { 161 | onError?.(flow, error); 162 | setError(error); 163 | } 164 | }; 165 | return { onSubmit, flow, setFlow, toggleFlow, error, setError, clearError }; 166 | } 167 | 168 | export function useSignOut() { 169 | const setSessionId = useSetSessionId(); 170 | return useCallback(() => setSessionId(null), [setSessionId]); 171 | } 172 | 173 | // Convex Hooks 174 | 175 | export function useQueryWithAuth< 176 | Args extends { sessionId: string | null }, 177 | Query extends FunctionReference<"query", "public", Args> 178 | >( 179 | query: Query, 180 | args: Omit 181 | ): Query["_returnType"] | undefined { 182 | const sessionId = useSessionId(); 183 | return useQuery(query, { ...args, sessionId } as any); 184 | } 185 | 186 | export function useMutationWithAuth< 187 | Args extends { sessionId: string | null }, 188 | Mutation extends FunctionReference<"mutation", "public", Args> 189 | >( 190 | mutation: Mutation 191 | ): ReactMutation< 192 | FunctionReference<"mutation", "public", Omit> 193 | > { 194 | const doMutation = useMutation(mutation); 195 | const sessionId = useSessionId(); 196 | return useCallback( 197 | (args: Omit) => { 198 | return doMutation({ ...args, sessionId } as any); 199 | }, 200 | [doMutation, sessionId] 201 | ) as any; // We don't support optimistic updates 202 | } 203 | -------------------------------------------------------------------------------- /cjs/react.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.useMutationWithAuth = exports.useQueryWithAuth = exports.useSignOut = exports.useSignUpSignIn = exports.SignOutButton = exports.SignUpSignIn = exports.setSavedSessionId = exports.SessionProvider = exports.useSetSessionId = exports.useSessionId = void 0; 13 | const jsx_runtime_1 = require("react/jsx-runtime"); 14 | const react_1 = require("convex/react"); 15 | const react_2 = require("react"); 16 | const SessionContext = (0, react_2.createContext)(undefined); 17 | function useSessionId() { 18 | return (0, react_2.useContext)(SessionContext).sessionId; 19 | } 20 | exports.useSessionId = useSessionId; 21 | function useSetSessionId() { 22 | return (0, react_2.useContext)(SessionContext).setSessionId; 23 | } 24 | exports.useSetSessionId = useSetSessionId; 25 | function SessionProvider({ children }) { 26 | const [sessionId, setSessionIdState] = (0, react_2.useState)(getSavedSessionId()); 27 | const setSessionId = (0, react_2.useCallback)((value) => { 28 | setSavedSessionId(value); 29 | setSessionIdState(value); 30 | }, [setSessionIdState]); 31 | return ((0, jsx_runtime_1.jsx)(SessionContext.Provider, { value: { sessionId, setSessionId }, children: children })); 32 | } 33 | exports.SessionProvider = SessionProvider; 34 | function getSavedSessionId() { 35 | return localStorage.getItem("sessionId"); 36 | } 37 | function setSavedSessionId(sessionId) { 38 | if (sessionId == null) { 39 | localStorage.removeItem("sessionId"); 40 | } 41 | else { 42 | localStorage.setItem("sessionId", sessionId); 43 | } 44 | } 45 | exports.setSavedSessionId = setSavedSessionId; 46 | // Sign in / sign up 47 | function SignUpSignIn({ labelClassName, inputClassName, buttonClassName, flowToggleClassName, errorDisplayClassName, signIn, signUp, onError, }) { 48 | const { flow, toggleFlow, onSubmit, error } = useSignUpSignIn({ 49 | signIn, 50 | signUp, 51 | onError, 52 | }); 53 | return ((0, jsx_runtime_1.jsxs)("form", { onSubmit: onSubmit, children: [(0, jsx_runtime_1.jsx)("label", { className: labelClassName, htmlFor: "username", children: "Email" }), (0, jsx_runtime_1.jsx)("input", { className: inputClassName, name: "email", id: "email", autoComplete: "username" }), (0, jsx_runtime_1.jsx)("label", { className: labelClassName, htmlFor: "password", children: "Password" }), (0, jsx_runtime_1.jsx)("input", { className: inputClassName, type: "password", name: "password", id: "password", autoComplete: flow === "signIn" ? "current-password" : "new-password" }), (0, jsx_runtime_1.jsx)("input", { className: buttonClassName, type: "submit", value: flow === "signIn" ? "Sign in" : "Sign up" }), (0, jsx_runtime_1.jsx)("a", { className: flowToggleClassName, onClick: toggleFlow, children: flow === "signIn" 54 | ? "Don't have an account? Sign up" 55 | : "Already have an account? Sign in" }), (0, jsx_runtime_1.jsx)("div", { className: errorDisplayClassName, children: error !== undefined 56 | ? flow === "signIn" 57 | ? "Could not sign in, did you mean to sign up?" 58 | : "Could not sign up, did you mean to sign in?" 59 | : null })] })); 60 | } 61 | exports.SignUpSignIn = SignUpSignIn; 62 | // Sign out 63 | function SignOutButton({ className }) { 64 | return ((0, jsx_runtime_1.jsx)("button", { className: className, onClick: useSignOut(), children: "Logout" })); 65 | } 66 | exports.SignOutButton = SignOutButton; 67 | // Hooks 68 | function useSignUpSignIn({ signIn, signUp, onError, }) { 69 | const setSessionId = useSetSessionId(); 70 | const [flow, setFlow] = (0, react_2.useState)("signIn"); 71 | const [error, setError] = (0, react_2.useState)(); 72 | const toggleFlow = (0, react_2.useCallback)(() => { 73 | setFlow(flow === "signIn" ? "signUp" : "signIn"); 74 | clearError(); 75 | }, [flow, setFlow]); 76 | const clearError = (0, react_2.useCallback)(() => setError(undefined), [setError]); 77 | const onSubmit = (event) => __awaiter(this, void 0, void 0, function* () { 78 | var _a, _b; 79 | event.preventDefault(); 80 | clearError(); 81 | const data = new FormData(event.currentTarget); 82 | try { 83 | const sessionId = yield (flow === "signIn" ? signIn : signUp)({ 84 | email: (_a = data.get("email")) !== null && _a !== void 0 ? _a : "", 85 | password: (_b = data.get("password")) !== null && _b !== void 0 ? _b : "", 86 | }); 87 | setSessionId(sessionId); 88 | } 89 | catch (error) { 90 | onError === null || onError === void 0 ? void 0 : onError(flow, error); 91 | setError(error); 92 | } 93 | }); 94 | return { onSubmit, flow, setFlow, toggleFlow, error, setError, clearError }; 95 | } 96 | exports.useSignUpSignIn = useSignUpSignIn; 97 | function useSignOut() { 98 | const setSessionId = useSetSessionId(); 99 | return (0, react_2.useCallback)(() => setSessionId(null), [setSessionId]); 100 | } 101 | exports.useSignOut = useSignOut; 102 | // Convex Hooks 103 | function useQueryWithAuth(query, args) { 104 | const sessionId = useSessionId(); 105 | return (0, react_1.useQuery)(query, Object.assign(Object.assign({}, args), { sessionId })); 106 | } 107 | exports.useQueryWithAuth = useQueryWithAuth; 108 | function useMutationWithAuth(mutation) { 109 | const doMutation = (0, react_1.useMutation)(mutation); 110 | const sessionId = useSessionId(); 111 | return (0, react_2.useCallback)((args) => { 112 | return doMutation(Object.assign(Object.assign({}, args), { sessionId })); 113 | }, [doMutation, sessionId]); // We don't support optimistic updates 114 | } 115 | exports.useMutationWithAuth = useMutationWithAuth; 116 | //# sourceMappingURL=react.js.map -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | # How to integrate Lucia auth into a Convex project 2 | 3 | ## (optional) Set up your schema 4 | 5 | You can skip this step if you're not using TypeScript. 6 | 7 | ### Set up the database schema 8 | 9 | In `convex/schema.ts`: 10 | 11 | ```ts 12 | import { defineSchema } from "convex/server"; 13 | import { authTables } from "@convex-dev/convex-lucia-auth"; 14 | import { v } from "convex/values"; 15 | 16 | export default defineSchema({ 17 | ...authTables({ 18 | user: { 19 | email: v.string(), 20 | }, 21 | session: {}, 22 | }), 23 | }); 24 | ``` 25 | 26 | ### Set up global types 27 | 28 | In `convex/env.d.ts`: 29 | 30 | ```ts 31 | declare namespace Lucia { 32 | type Auth = import("@convex-dev/convex-lucia-auth").Auth; 33 | type DatabaseUserAttributes = 34 | import("@convex-dev/convex-lucia-auth").DatabaseUserAttributes & { 35 | email: string; 36 | }; 37 | type DatabaseSessionAttributes = 38 | import("@convex-dev/convex-lucia-auth").DatabaseSessionAttributes; 39 | } 40 | 41 | declare namespace ConvexLuciaAuth { 42 | type DataModel = import("./_generated/dataModel").DataModel; 43 | } 44 | ``` 45 | 46 | ## Implement Sign Up / Sign In / Log out 47 | 48 | ### Backend 49 | 50 | Implement public mutations for the three operations using `convex-lucia-auth` 51 | and `convex-lucia-auth/email`, which return a `SessionId`. 52 | 53 | In `convex/users.ts` or similar: 54 | 55 | ```ts 56 | import { v } from "convex/values"; 57 | import { queryWithAuth, mutationWithAuth } from "@convex-dev/convex-lucia-auth"; 58 | import { 59 | signInWithEmailAndPassword, 60 | signUpWithEmailAndPassword, 61 | } from "@convex-dev/convex-lucia-auth/email"; 62 | 63 | export const signIn = mutationWithAuth({ 64 | args: { 65 | email: v.string(), 66 | password: v.string(), 67 | }, 68 | handler: async (ctx, { email, password }) => { 69 | const session = await signInWithEmailAndPassword(ctx, email, password); 70 | return session.sessionId; 71 | }, 72 | }); 73 | 74 | export const signUp = mutationWithAuth({ 75 | args: { 76 | email: v.string(), 77 | password: v.string(), 78 | }, 79 | handler: async (ctx, { email, password }) => { 80 | const session = await signUpWithEmailAndPassword(ctx, email, password); 81 | return session.sessionId; 82 | }, 83 | }); 84 | ``` 85 | 86 | ### Frontend 87 | 88 | Wrap your app in `SessionProvider` from `convex-lucia-auth/react`. 89 | 90 | In `main.tsx` or similar: 91 | 92 | ```tsx 93 | import { ConvexProvider, ConvexReactClient } from "convex/react"; 94 | import React from "react"; 95 | import ReactDOM from "react-dom/client"; 96 | import { SessionProvider } from "@convex-dev/convex-lucia-auth/react"; 97 | import App from "./App"; 98 | 99 | const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string); 100 | 101 | ReactDOM.createRoot(document.getElementById("root")!).render( 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | ); 110 | ``` 111 | 112 | In your app use the `SignInSignUp` component from `convex-lucia-auth/react`. 113 | 114 | In `AuthForm.tsx` or similar: 115 | 116 | ```tsx 117 | import { 118 | SignUpSignIn, 119 | useMutationWithAuth, 120 | } from "@convex-dev/convex-lucia-auth/react"; 121 | // This path is relative so you might need to update it: 122 | import { api } from "../convex/_generated/api"; 123 | 124 | export function AuthForm() { 125 | const signIn = useMutationWithAuth(api.users.signIn); 126 | const signUp = useMutationWithAuth(api.users.signUp); 127 | return ; 128 | } 129 | ``` 130 | 131 | Similarly you can use the `SignOutButton` component. 132 | 133 | ```tsx 134 | import { SignOutButton } from "@convex-dev/convex-lucia-auth/react"; 135 | ``` 136 | 137 | ## Check whether the user is signed in 138 | 139 | ### Backend 140 | 141 | Add a query exposing whatever information about the current user your frontend 142 | needs. In this example we expose the whole user document, from `ctx.session`. 143 | 144 | In `convex/users.ts` or similar: 145 | 146 | ```tsx 147 | import { queryWithAuth } from "@convex-dev/convex-lucia-auth"; 148 | 149 | export const get = queryWithAuth({ 150 | args: {}, 151 | handler: async (ctx) => { 152 | return ctx.session?.user; 153 | }, 154 | }); 155 | ``` 156 | 157 | ### Frontend 158 | 159 | Leverage the query, possibly rendering the `SignUpSignIn` component when the 160 | user isn't logged in. 161 | 162 | In `src/App.tsx` or similar: 163 | 164 | ```tsx 165 | import { useQueryWithAuth } from "@convex-dev/convex-lucia-auth/react"; 166 | // This path is relative so you might need to update it: 167 | import { api } from "../convex/_generated/api"; 168 | import { AuthForm } from "./AuthForm"; 169 | 170 | export function App() { 171 | const user = useQueryWithAuth(api.users.get, {}); 172 | 173 | return user === undefined ? ( 174 | <>Loading... 175 | ) : user === null ? ( 176 | 177 | ) : ( 178 | <> 179 | <>Signed in with email: {user.email} 180 | 181 | 182 | ); 183 | } 184 | ``` 185 | 186 | ## Clearing old dead sessions 187 | 188 | Every time a user signs in a session is created for them. It is a good idea to 189 | delete old sessions so that they don't accummulate indefinitely, using 190 | `findAndDeleteDeadUserSessions` from `convex-lucia-auth`. 191 | 192 | In `convex/crons.ts`: 193 | 194 | ```ts 195 | import { cronJobs } from "convex/server"; 196 | import { internal } from "./_generated/api"; 197 | import { internalMutation } from "./_generated/server"; 198 | import { findAndDeleteDeadUserSessions } from "@convex-dev/convex-lucia-auth"; 199 | 200 | const crons = cronJobs(); 201 | 202 | crons.daily( 203 | "clear stale sessions and keys", 204 | { hourUTC: 8, minuteUTC: 0 }, 205 | internal.crons.clearStaleSessionsAndKeys 206 | ); 207 | 208 | export const clearStaleSessionsAndKeys = internalMutation( 209 | findAndDeleteDeadUserSessions 210 | ); 211 | 212 | export default crons; 213 | ``` 214 | 215 | ## Advanced 216 | 217 | ### Customize frontend 218 | 219 | To customize the UX appearance, you can either: 220 | 221 | 1. Specify classnames props for each component 222 | 2. Use hooks and use your own components 223 | 224 | For the second approach, follow these recipes: 225 | 226 | #### Using custom components for sign up and sign in 227 | 228 | In `src/CustomAuthForm.tsx` or similar: 229 | 230 | ```tsx 231 | import { useSignUpSignIn } from "@convex-dev/convex-lucia-auth/react"; 232 | 233 | export function CustomAuthForm() { 234 | const { flow, toggleFlow, error, onSubmit } = useSignUpSignIn({ 235 | signIn: useMutationWithAuth(api.auth.signIn), 236 | signUp: useMutationWithAuth(api.auth.signUp), 237 | }); 238 | return ( 239 | <> 240 |
241 | 242 | 243 | 244 | 245 | 249 | 250 | 251 | {flow === "signIn" 252 | ? "Don't have an account? Sign up" 253 | : "Already have an account? Sign in"} 254 | 255 | {error !== undefined 256 | ? flow === "signIn" 257 | ? "Could not sign in, did you mean to sign up?" 258 | : "Could not sign up, did you mean to sign in?" 259 | : null} 260 | 261 | ); 262 | } 263 | ``` 264 | 265 | #### Custom SignOutButton 266 | 267 | In `src/CustomSignOutButton.tsx` or similar: 268 | 269 | ```tsx 270 | import { useSignOut } from "@convex-dev/convex-lucia-auth/react"; 271 | 272 | export function CustomSignOutButton() { 273 | return Logout; 274 | } 275 | ``` 276 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DataModelFromSchemaDefinition, 3 | GenericDatabaseReader as DatabaseReader, 4 | GenericDatabaseWriter as DatabaseWriter, 5 | DocumentByName, 6 | GenericMutationCtx as MutationCtx, 7 | GenericQueryCtx as QueryCtx, 8 | defineSchema, 9 | defineTable, 10 | internalMutationGeneric as internalMutation, 11 | internalQueryGeneric as internalQuery, 12 | mutationGeneric as mutation, 13 | queryGeneric as query, 14 | } from "convex/server"; 15 | import { ObjectType, PropertyValidators, Validator, v } from "convex/values"; 16 | import { Session } from "lucia"; 17 | 18 | export type DatabaseUserAttributes = Omit< 19 | DocumentByName, 20 | "id" 21 | >; 22 | 23 | export type DatabaseSessionAttributes = Omit< 24 | DocumentByName, 25 | "id" | "user_id" | "active_expires" | "idle_expires" 26 | >; 27 | 28 | export function queryWithAuth< 29 | ArgsValidator extends PropertyValidators, 30 | Output, 31 | >({ 32 | args, 33 | handler, 34 | }: { 35 | args: ArgsValidator; 36 | handler: ( 37 | ctx: Omit, "auth"> & { 38 | session: Session | null; 39 | }, 40 | args: ObjectType 41 | ) => Output; 42 | }) { 43 | return query({ 44 | args: { 45 | ...args, 46 | sessionId: v.union(v.null(), v.string()), 47 | }, 48 | handler: async (ctx, args: any) => { 49 | const session = await getValidExistingSession(ctx, args.sessionId); 50 | return handler({ ...ctx, session }, args); 51 | }, 52 | }); 53 | } 54 | 55 | export function internalQueryWithAuth< 56 | ArgsValidator extends PropertyValidators, 57 | Output, 58 | >({ 59 | args, 60 | handler, 61 | }: { 62 | args: ArgsValidator; 63 | handler: ( 64 | ctx: Omit, "auth"> & { 65 | session: Session | null; 66 | }, 67 | args: ObjectType 68 | ) => Output; 69 | }) { 70 | return internalQuery({ 71 | args: { ...args, sessionId: v.union(v.null(), v.string()) }, 72 | handler: async (ctx, args: any) => { 73 | const session = await getValidExistingSession(ctx, args.sessionId); 74 | return handler({ ...ctx, session }, args); 75 | }, 76 | }); 77 | } 78 | 79 | export function mutationWithAuth< 80 | ArgsValidator extends PropertyValidators, 81 | Output, 82 | >({ 83 | args, 84 | handler, 85 | }: { 86 | args: ArgsValidator; 87 | handler: ( 88 | ctx: Omit, "auth"> & { 89 | auth: Auth; 90 | session: Session | null; 91 | }, 92 | args: ObjectType 93 | ) => Output; 94 | }) { 95 | return mutation({ 96 | args: { ...args, sessionId: v.union(v.null(), v.string()) }, 97 | handler: async (ctx, args: any) => { 98 | const auth = getAuth(ctx.db); 99 | const session = await getValidSessionAndRenew(auth, args.sessionId); 100 | return handler({ ...ctx, session, auth }, args); 101 | }, 102 | }); 103 | } 104 | 105 | export function internalMutationWithAuth< 106 | ArgsValidator extends PropertyValidators, 107 | Output, 108 | >({ 109 | args, 110 | handler, 111 | }: { 112 | args: ArgsValidator; 113 | handler: ( 114 | ctx: Omit, "auth"> & { 115 | auth: Auth; 116 | session: Session | null; 117 | }, 118 | args: ObjectType 119 | ) => Output; 120 | }) { 121 | return internalMutation({ 122 | args: { ...args, sessionId: v.union(v.null(), v.string()) }, 123 | handler: async (ctx, args: any) => { 124 | const auth = getAuth(ctx.db); 125 | const session = await getValidSessionAndRenew(auth, args.sessionId); 126 | return handler({ ...ctx, session, auth }, args); 127 | }, 128 | }); 129 | } 130 | 131 | async function getValidExistingSession( 132 | ctx: QueryCtx, 133 | sessionId: string | null 134 | ) { 135 | if (sessionId === null) { 136 | return null; 137 | } 138 | // The cast is OK because we will only expose the existing session 139 | const auth = getAuth(ctx.db as DatabaseWriter); 140 | try { 141 | const session = (await auth.getSession(sessionId)) as Session | null; 142 | if (session === null || session.state === "idle") { 143 | return null; 144 | } 145 | return session; 146 | } catch (error) { 147 | // Invalid session ID 148 | return null; 149 | } 150 | } 151 | 152 | export async function getValidSessionAndRenew( 153 | auth: Auth, 154 | sessionId: string | null 155 | ) { 156 | if (sessionId === null) { 157 | return null; 158 | } 159 | try { 160 | return await auth.validateSession(sessionId); 161 | } catch (error) { 162 | // Invalid session ID 163 | return null; 164 | } 165 | } 166 | 167 | // lucia.ts 168 | import { 169 | Adapter, 170 | KeySchema, 171 | LuciaErrorConstructor, 172 | SessionSchema, 173 | UserSchema, 174 | lucia, 175 | } from "lucia"; 176 | 177 | type SessionId = string; 178 | type UserId = string; 179 | type KeyId = string; 180 | 181 | const minimalSchema = defineSchema({ 182 | ...authTables({ 183 | user: {}, 184 | session: {}, 185 | }), 186 | }); 187 | 188 | export type MinimalDataModel = DataModelFromSchemaDefinition< 189 | typeof minimalSchema 190 | >; 191 | 192 | export function authTables< 193 | UserFields extends Record>, 194 | SchemaFields extends Record>, 195 | >({ user, session }: { user: UserFields; session: SchemaFields }) { 196 | return { 197 | users: defineTable({ 198 | ...user, 199 | id: v.string(), 200 | }).index("byId", ["id"]), 201 | sessions: defineTable({ 202 | ...session, 203 | id: v.string(), 204 | user_id: v.string(), 205 | active_expires: v.float64(), 206 | idle_expires: v.float64(), 207 | }) 208 | // `as any` because TypeScript can't infer the table fields correctly 209 | .index("byId", ["id" as any]) 210 | .index("byUserId", ["user_id" as any]), 211 | auth_keys: defineTable({ 212 | id: v.string(), 213 | hashed_password: v.union(v.string(), v.null()), 214 | user_id: v.string(), 215 | }) 216 | .index("byId", ["id"]) 217 | .index("byUserId", ["user_id"]), 218 | }; 219 | } 220 | 221 | // Set the LUCIA_ENVIRONMENT variable to "PROD" 222 | // on your prod deployment's dashboard 223 | export function getAuth(db: DatabaseWriter) { 224 | return lucia({ 225 | adapter: convexAdapter(db), 226 | env: (process.env.LUCIA_ENVIRONMENT as "PROD" | undefined) ?? "DEV", 227 | getUserAttributes(user: UserSchema) { 228 | return user; 229 | }, 230 | getSessionAttributes(session: SessionSchema) { 231 | return session; 232 | }, 233 | }); 234 | } 235 | 236 | export type Auth = ReturnType; 237 | 238 | export function convexAdapter(db: DatabaseWriter) { 239 | return (LuciaError: LuciaErrorConstructor): Adapter => ({ 240 | async getSessionAndUser( 241 | sessionId: string 242 | ): Promise<[SessionSchema, UserSchema] | [null, null]> { 243 | const session = await getSession(db, sessionId); 244 | if (session === null) { 245 | return [null, null]; 246 | } 247 | const user = await getUser(db, session.user_id); 248 | if (user === null) { 249 | return [null, null]; 250 | } 251 | return [session, user]; 252 | }, 253 | async deleteSession(sessionId: SessionId): Promise { 254 | const session = await getSession(db, sessionId); 255 | if (session === null) { 256 | return; 257 | } 258 | await db.delete(session._id); 259 | }, 260 | async deleteSessionsByUserId(userId: UserId): Promise { 261 | const sessions = await this.getSessionsByUserId(userId); 262 | await Promise.all(sessions.map((session) => db.delete(session._id))); 263 | }, 264 | async getSession(sessionId: SessionId): Promise { 265 | return await getSession(db, sessionId); 266 | }, 267 | async getSessionsByUserId(userId: UserId): Promise { 268 | return await db 269 | .query("sessions") 270 | .withIndex("byUserId", (q) => q.eq("user_id", userId)) 271 | .collect(); 272 | }, 273 | async setSession(session: SessionSchema): Promise { 274 | const { _id, _creationTime, ...data } = session; 275 | await db.insert("sessions", data); 276 | }, 277 | async deleteKeysByUserId(userId: UserId): Promise { 278 | const keys = await db 279 | .query("auth_keys") 280 | .withIndex("byUserId", (q) => q.eq("user_id", userId)) 281 | .collect(); 282 | await Promise.all(keys.map((key) => db.delete(key._id))); 283 | }, 284 | async deleteKey(keyId: KeyId): Promise { 285 | const key = await getKey(db, keyId); 286 | if (key === null) { 287 | return; 288 | } 289 | await db.delete(key._id); 290 | }, 291 | async deleteUser(userId: UserId): Promise { 292 | const user = await getUser(db, userId); 293 | if (user === null) { 294 | return; 295 | } 296 | await db.delete(user._id); 297 | }, 298 | async getKey(keyId: KeyId): Promise { 299 | return await getKey(db, keyId); 300 | }, 301 | async getKeysByUserId(userId: UserId): Promise { 302 | return await db 303 | .query("auth_keys") 304 | .withIndex("byUserId", (q) => q.eq("user_id", userId)) 305 | .collect(); 306 | }, 307 | async getUser(userId: UserId): Promise { 308 | return await getUser(db, userId); 309 | }, 310 | async setKey(key: KeySchema): Promise { 311 | const existingKey = await this.getKey(key.id); 312 | if (existingKey !== null) { 313 | throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); 314 | } 315 | const user = await this.getUser(key.user_id); 316 | if (user === null) { 317 | throw new LuciaError("AUTH_INVALID_USER_ID"); 318 | } 319 | await db.insert("auth_keys", key); 320 | }, 321 | async setUser(user: UserSchema, key: KeySchema | null): Promise { 322 | const { _id, _creationTime, ...data } = user; 323 | await db.insert("users", data); 324 | if (key !== null) { 325 | await this.setKey(key); 326 | } 327 | }, 328 | async updateKey( 329 | keyId: string, 330 | partialKey: Partial 331 | ): Promise { 332 | const key = await getKey(db, keyId); 333 | if (key === null) { 334 | throw new LuciaError("AUTH_INVALID_KEY_ID"); 335 | } 336 | await db.patch(key._id, partialKey); 337 | }, 338 | async updateUser( 339 | userId: string, 340 | partialUser: Partial 341 | ): Promise { 342 | const user = await getUser(db, userId); 343 | if (user === null) { 344 | throw new LuciaError("AUTH_INVALID_USER_ID"); 345 | } 346 | await db.patch(user._id, partialUser); 347 | }, 348 | async updateSession( 349 | sessionId: string, 350 | partialSession: Partial 351 | ): Promise { 352 | const session = await getSession(db, sessionId); 353 | if (session === null) { 354 | throw new LuciaError("AUTH_INVALID_SESSION_ID"); 355 | } 356 | await db.patch(session._id, partialSession); 357 | }, 358 | }); 359 | } 360 | 361 | export async function getSession( 362 | db: DatabaseReader, 363 | sessionId: string 364 | ) { 365 | return await db 366 | .query("sessions") 367 | .withIndex("byId", (q) => q.eq("id", sessionId)) 368 | .first(); 369 | } 370 | 371 | export async function getUser( 372 | db: DatabaseReader, 373 | userId: string 374 | ) { 375 | return await db 376 | .query("users") 377 | .withIndex("byId", (q) => q.eq("id", userId)) 378 | .first(); 379 | } 380 | 381 | export async function getKey( 382 | db: DatabaseReader, 383 | keyId: string 384 | ) { 385 | return await db 386 | .query("auth_keys") 387 | .withIndex("byId", (q) => q.eq("id", keyId)) 388 | .first(); 389 | } 390 | 391 | export async function findAndDeleteDeadUserSessions(ctx: { 392 | db: DatabaseWriter; 393 | }) { 394 | const sessions = await ctx.db.query("sessions").collect(); 395 | for (const session of sessions) { 396 | await getAuth(ctx.db).deleteDeadUserSessions(session.user_id); 397 | } 398 | } 399 | --------------------------------------------------------------------------------