diff --git a/.auri/$3s4lqgi3.md b/.auri/$3s4lqgi3.md new file mode 100644 index 000000000..6dddbea7a --- /dev/null +++ b/.auri/$3s4lqgi3.md @@ -0,0 +1,8 @@ +--- +package: "@lucia-auth/adapter-session-redis" # package name +type: "minor" # "major", "minor", "patch" +--- + + +Add Redis adapter for Upstash + diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/astro.md b/documentation-v2/content/main/0.start-here/0.getting-started/astro.md index 63e3036fb..0b20e8d9b 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/astro.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/astro.md @@ -57,7 +57,7 @@ const auth = lucia({ - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL -- [`postgres`](https://github.com/porsager/postgres): PostgreSQL +- [`postgres`](/database-adapters/postgres): PostgreSQL - [Prisma](/database-adapters/prisma): MongoDB, MySQL, PostgreSQL, SQLite - [Redis](/database-adapters/redis): Redis @@ -65,6 +65,7 @@ const auth = lucia({ - [Cloudflare D1](/database-adapters/cloudflare-d1) - [PlanetScale serverless](/database-adapters/planetscale-serverless) +- [Upstash](/database-adapters/upstash) ## Set up types diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/express.md b/documentation-v2/content/main/0.start-here/0.getting-started/express.md index 2f6fe3d46..73826aff8 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/express.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/express.md @@ -68,7 +68,7 @@ const auth = lucia({ - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL -- [`postgres`](https://github.com/porsager/postgres): PostgreSQL +- [`postgres`](/database-adapters/postgres): PostgreSQL - [Prisma](/database-adapters/prisma): MongoDB, MySQL, PostgreSQL, SQLite - [Redis](/database-adapters/redis): Redis @@ -76,6 +76,7 @@ const auth = lucia({ - [Cloudflare D1](/database-adapters/cloudflare-d1) - [PlanetScale serverless](/database-adapters/planetscale-serverless) +- [Upstash](/database-adapters/upstash) ## Set up types diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/index.md b/documentation-v2/content/main/0.start-here/0.getting-started/index.md index fadf11add..77c732df8 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/index.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/index.md @@ -87,7 +87,7 @@ const auth = lucia({ - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL -- [`postgres`](https://github.com/porsager/postgres): PostgreSQL +- [`postgres`](/database-adapters/postgres): PostgreSQL - [Prisma](/database-adapters/prisma): MongoDB, MySQL, PostgreSQL, SQLite - [Redis](/database-adapters/redis): Redis @@ -95,6 +95,7 @@ const auth = lucia({ - [Cloudflare D1](/database-adapters/cloudflare-d1) - [PlanetScale serverless](/database-adapters/planetscale-serverless) +- [Upstash](/database-adapters/upstash) ## Set up types diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-app.md b/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-app.md index 2db9f9ee5..96dd32a3a 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-app.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-app.md @@ -63,7 +63,7 @@ const auth = lucia({ - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL -- [`postgres`](https://github.com/porsager/postgres): PostgreSQL +- [`postgres`](/database-adapters/postgres): PostgreSQL - [Prisma](/database-adapters/prisma): MongoDB, MySQL, PostgreSQL, SQLite - [Redis](/database-adapters/redis): Redis @@ -71,6 +71,7 @@ const auth = lucia({ - [Cloudflare D1](/database-adapters/cloudflare-d1) - [PlanetScale serverless](/database-adapters/planetscale-serverless) +- [Upstash](/database-adapters/upstash) ## Set up types diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-pages.md b/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-pages.md index 6b629e573..ae7ec129c 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-pages.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/nextjs-pages.md @@ -63,7 +63,7 @@ const auth = lucia({ - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL -- [`postgres`](https://github.com/porsager/postgres): PostgreSQL +- [`postgres`](/database-adapters/postgres): PostgreSQL - [Prisma](/database-adapters/prisma): MongoDB, MySQL, PostgreSQL, SQLite - [Redis](/database-adapters/redis): Redis @@ -71,6 +71,7 @@ const auth = lucia({ - [Cloudflare D1](/database-adapters/cloudflare-d1) - [PlanetScale serverless](/database-adapters/planetscale-serverless) +- [Upstash](/database-adapters/upstash) ## Set up types diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/nuxt.md b/documentation-v2/content/main/0.start-here/0.getting-started/nuxt.md index e8e41bb27..9d827cb8e 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/nuxt.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/nuxt.md @@ -57,7 +57,7 @@ const auth = lucia({ - [libSQL](/database-adapters/libSQL): libSQL (Turso) - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL -- [`postgres`](https://github.com/porsager/postgres): PostgreSQL +- [`postgres`](/database-adapters/postgres): PostgreSQL - [Prisma](/database-adapters/prisma): MongoDB, MySQL, PostgreSQL, SQLite - [Redis](/database-adapters/redis): Redis @@ -65,6 +65,7 @@ const auth = lucia({ - [Cloudflare D1](/database-adapters/cloudflare-d1) - [PlanetScale serverless](/database-adapters/planetscale-serverless) +- [Upstash](/database-adapters/upstash) ## Set up types diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/remix.md b/documentation-v2/content/main/0.start-here/0.getting-started/remix.md index 620c6a8ff..567288445 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/remix.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/remix.md @@ -79,7 +79,7 @@ const auth = lucia({ - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL -- [`postgres`](https://github.com/porsager/postgres): PostgreSQL +- [`postgres`](/database-adapters/postgres): PostgreSQL - [Prisma](/database-adapters/prisma): MongoDB, MySQL, PostgreSQL, SQLite - [Redis](/database-adapters/redis): Redis @@ -87,6 +87,7 @@ const auth = lucia({ - [Cloudflare D1](/database-adapters/cloudflare-d1) - [PlanetScale serverless](/database-adapters/planetscale-serverless) +- [Upstash](/database-adapters/upstash) ## Set up types diff --git a/documentation-v2/content/main/0.start-here/0.getting-started/sveltekit.md b/documentation-v2/content/main/0.start-here/0.getting-started/sveltekit.md index 6351bbdd3..db5fff122 100644 --- a/documentation-v2/content/main/0.start-here/0.getting-started/sveltekit.md +++ b/documentation-v2/content/main/0.start-here/0.getting-started/sveltekit.md @@ -60,7 +60,7 @@ const auth = lucia({ - [Mongoose](/database-adapters/mongoose): MongoDB - [`mysql2`](/database-adapters/mysql2): MySQL - [`pg`](/database-adapters/pg): PostgreSQL -- [`postgres`](https://github.com/porsager/postgres): PostgreSQL +- [`postgres`](/database-adapters/postgres): PostgreSQL - [Prisma](/database-adapters/prisma): MongoDB, MySQL, PostgreSQL, SQLite - [Redis](/database-adapters/redis): Redis @@ -68,6 +68,7 @@ const auth = lucia({ - [Cloudflare D1](/database-adapters/cloudflare-d1) - [PlanetScale serverless](/database-adapters/planetscale-serverless) +- [Upstash](/database-adapters/upstash) ## Set up types diff --git a/documentation-v2/content/main/1.basics/0.database.md b/documentation-v2/content/main/1.basics/0.database.md index 69aaf125c..67205302c 100644 --- a/documentation-v2/content/main/1.basics/0.database.md +++ b/documentation-v2/content/main/1.basics/0.database.md @@ -18,10 +18,11 @@ We currently provide the following adapters: - [Mongoose](/database-adapters/mongoose) - [`mysql2`](/database-adapters/mysql2) - [`pg`](/database-adapters/pg) -- [`postgres`](https://github.com/porsager/postgres) +- [`postgres`](/database-adapters/postgres) - [PlanetScale serverless](/database-adapters/planetscale-serverless) - [Prisma](/database-adapters/prisma) - [Redis](/database-adapters/redis) +- [Upstash](/database-adapters/upstash) You can also use query builders like Drizzle ORM and Kysely since they rely on underlying drivers that we provide adapters for. diff --git a/documentation-v2/content/main/2.database-adapters/upstash.md b/documentation-v2/content/main/2.database-adapters/upstash.md new file mode 100644 index 000000000..e46eae405 --- /dev/null +++ b/documentation-v2/content/main/2.database-adapters/upstash.md @@ -0,0 +1,52 @@ +--- +order: 0 +title: "Upstash" +description: "Learn how to use Upstas Redis with Lucia" +--- + +Session adapter for [Upstash Redis](https://upstash.com) provided by the Redis session adapter package. This only handles sessions, and not users or keys. + +```ts +import { upstash } from "@lucia-auth/adapter-session-redis"; +``` + +```ts +const upstash: ( + upstashClient: Redis, + prefixes?: { + session: string; + userSessions: string; + } +) => InitializeAdapter; +``` + +##### Parameters + +| name | type | optional | description | +| --------------- | ------------------------ | :------: | ------------------------------------ | +| `upstashClient` | `Redis` | | Serverless redis client for upstash. | +| `prefixes` | `Record` | ✓ | Key prefixes | + +### Key prefixes + +Key are defined as a combination of a prefix and an id so everything can be stored in a single Redis instance. By default, sessions are stored as `session:` and user-sessions relationships are stored as `user_sessions:`. + +## Usage + +```ts +import { lucia } from "lucia"; +import { upstash } from "@lucia-auth/adapter-session-redis"; +import { Redis } from "@upstash/redis"; + +const upstashClient = new Redis({ + // ... +}); + +const auth = lucia({ + adapter: { + user: userAdapter, // any normal adapter for storing users/keys + session: upstash(upstashClient) + } + // ... +}); +``` diff --git a/packages/adapter-session-redis/.env.example b/packages/adapter-session-redis/.env.example index 634f46e4c..5894a4896 100644 --- a/packages/adapter-session-redis/.env.example +++ b/packages/adapter-session-redis/.env.example @@ -1 +1,6 @@ -REDIS_PORT="" \ No newline at end of file +# for Redis adapter +REDIS_PORT="your_redis_port" + +# for Upstash adapter +URL="your_upstash_url" +TOKEN="your_upstash_token" \ No newline at end of file diff --git a/packages/adapter-session-redis/README.md b/packages/adapter-session-redis/README.md index a77467f2d..9d58bf5d1 100644 --- a/packages/adapter-session-redis/README.md +++ b/packages/adapter-session-redis/README.md @@ -1,25 +1,36 @@ # `@lucia-auth/adapter-session-redis` -[Redis](https://redis.io) session adapter for Lucia - -**[Documentation](https://lucia-auth.com/database/redis)** +Redis session adapters for Lucia **[Lucia documentation](https://lucia-auth.com)** **[Changelog](https://github.com/pilcrowOnPaper/lucia/blob/main/packages/session-adapter-redis/CHANGELOG.md)** +## Included adapters + +- [Redis](https://redis.io) ([Documentation](https://v2.lucia-auth.com/database-adapters/redis)) +- [Upstash](https://upstash.com) ([Documentation](https://v2.lucia-auth.com/database-adapters/upstash)) + ## Installation ``` npm install @lucia-auth/adapter-session-redis ``` -`lucia-auth@0.11.x` recommended. - ## Testing -Add a postgresql database url to `.env`. +### Redis + +Add the port of a local Redis DB to `.env`. + +``` +npm run test.redis +``` + +### Upstash + +Add the `UPSTASH_REDIS_REST_URL` and the `UPSTASH_REDIS_REST_TOKEN` to `.env`. ``` -npm run test +npm run test.upstash ``` diff --git a/packages/adapter-session-redis/package.json b/packages/adapter-session-redis/package.json index 3496f51c1..0e40842c9 100644 --- a/packages/adapter-session-redis/package.json +++ b/packages/adapter-session-redis/package.json @@ -8,11 +8,14 @@ "type": "module", "files": [ "/dist/", - "CHANGELOG.md" + "CHANGELOG.md", + "README.md" ], "scripts": { "build": "shx rm -rf ./dist/* && tsc", - "test": "tsx test/index.ts", + "test": "pnpm test.redis && pnpm test.upstash", + "test.redis": "tsx test/redis.ts", + "test.upstash": "tsx test/upstash.ts", "auri.build": "pnpm build" }, "keywords": [ @@ -22,7 +25,8 @@ "authentication", "adapter", "redis", - "session" + "session", + "upstash" ], "repository": { "type": "git", @@ -36,13 +40,23 @@ }, "peerDependencies": { "lucia": "2.0.0-beta.4", - "redis": "^4.0.0" + "redis": "^4.0.0", + "@upstash/redis": "^1.20.0" + }, + "peerDependenciesMeta": { + "redis": { + "optional": true + }, + "@upstash/redis": { + "optional": true + } }, "devDependencies": { - "@lucia-auth/adapter-test": "latest", + "@lucia-auth/adapter-test": "workspace:*", + "@upstash/redis": "^1.21.0", "dotenv": "^16.0.3", + "lucia": "workspace:*", "redis": "^4.3.1", - "tsx": "^3.12.6", - "lucia": "latest" + "tsx": "^3.12.6" } } diff --git a/packages/adapter-session-redis/src/redis.ts b/packages/adapter-session-redis/src/drivers/redis.ts similarity index 97% rename from packages/adapter-session-redis/src/redis.ts rename to packages/adapter-session-redis/src/drivers/redis.ts index 0344600f7..ae266ca0e 100644 --- a/packages/adapter-session-redis/src/redis.ts +++ b/packages/adapter-session-redis/src/drivers/redis.ts @@ -1,4 +1,4 @@ -import type { SessionSchema, SessionAdapter, InitializeAdapter } from "lucia"; +import type { InitializeAdapter, SessionAdapter, SessionSchema } from "lucia"; import type { RedisClientType } from "redis"; export const DEFAULT_SESSION_PREFIX = "session"; diff --git a/packages/adapter-session-redis/src/drivers/upstash.ts b/packages/adapter-session-redis/src/drivers/upstash.ts new file mode 100644 index 000000000..eec5a2f53 --- /dev/null +++ b/packages/adapter-session-redis/src/drivers/upstash.ts @@ -0,0 +1,88 @@ +import type { Redis } from "@upstash/redis"; +import type { InitializeAdapter, SessionAdapter, SessionSchema } from "lucia"; + +export const DEFAULT_SESSION_PREFIX = "session"; +export const DEFAULT_USER_SESSIONS_PREFIX = "user_sessions"; + +export const upstashSessionAdapter = ( + upstashClient: Redis, + prefixes?: { + session: string; + userSessions: string; + } +): InitializeAdapter => { + return () => { + const sessionKey = (sessionId: string) => { + return [prefixes?.session ?? DEFAULT_SESSION_PREFIX, sessionId].join(":"); + }; + const userSessionsKey = (userId: string) => { + return [ + prefixes?.userSessions ?? DEFAULT_USER_SESSIONS_PREFIX, + userId + ].join(":"); + }; + + return { + getSession: async (sessionId) => { + const sessionData = await upstashClient.get(sessionKey(sessionId)); + if (!sessionData) return null; + return sessionData as SessionSchema; + }, + getSessionsByUserId: async (userId) => { + const sessionIds = await upstashClient.smembers( + userSessionsKey(userId) + ); + if (sessionIds.length === 0) return []; + const pipeline = upstashClient.pipeline(); + for (const sessionId of sessionIds) { + pipeline.get(sessionKey(sessionId)) + } + const sessions = await pipeline.exec(); + return sessions; + }, + setSession: async (session) => { + const pipeline = upstashClient.pipeline(); + pipeline.sadd(userSessionsKey(session.user_id), session.id); + pipeline.set(sessionKey(session.id), JSON.stringify(session), { + ex: Math.floor(Number(session.idle_expires) / 1000) + }); + await pipeline.exec(); + }, + deleteSession: async (sessionId) => { + const session = await upstashClient.get( + sessionKey(sessionId) + ); + if (!session) return; + const pipeline = upstashClient.pipeline(); + pipeline.del(sessionKey(sessionId)); + pipeline.srem(userSessionsKey(session.user_id), sessionId); + await pipeline.exec(); + }, + deleteSessionsByUserId: async (userId) => { + const sessionIds = await upstashClient.smembers( + userSessionsKey(userId) + ); + const pipeline = upstashClient.pipeline(); + for (const sessionId of sessionIds) { + pipeline.del(sessionKey(sessionId)) + } + pipeline.del(userSessionsKey(userId)); + await pipeline.exec(); + }, + updateSession: async (sessionId, partialSession) => { + const session = await upstashClient.get( + sessionKey(sessionId) + ); + if (!session) return; + const updatedSession = { ...session, ...partialSession }; + await upstashClient.set( + sessionKey(sessionId), + JSON.stringify(updatedSession), + { + ex: Math.floor(Number(updatedSession.idle_expires) / 1000) + } + ); + } + }; + }; +}; diff --git a/packages/adapter-session-redis/src/index.ts b/packages/adapter-session-redis/src/index.ts index 23c8cabc4..fa5fb6297 100644 --- a/packages/adapter-session-redis/src/index.ts +++ b/packages/adapter-session-redis/src/index.ts @@ -1 +1,2 @@ -export { redisSessionAdapter as redis } from "./redis.js"; +export { redisSessionAdapter as redis } from "./drivers/redis.js"; +export { upstashSessionAdapter as upstash } from "./drivers/upstash.js"; diff --git a/packages/adapter-session-redis/test/index.ts b/packages/adapter-session-redis/test/redis.ts similarity index 86% rename from packages/adapter-session-redis/test/index.ts rename to packages/adapter-session-redis/test/redis.ts index cbd55491d..af405522f 100644 --- a/packages/adapter-session-redis/test/index.ts +++ b/packages/adapter-session-redis/test/redis.ts @@ -1,14 +1,14 @@ -import { testSessionAdapter, Database } from "@lucia-auth/adapter-test"; -import { LuciaError } from "lucia"; +import { Database, testSessionAdapter } from "@lucia-auth/adapter-test"; import dotenv from "dotenv"; +import { LuciaError } from "lucia"; import { resolve } from "path"; - import { createClient } from "redis"; + import { - redisSessionAdapter, DEFAULT_SESSION_PREFIX, - DEFAULT_USER_SESSIONS_PREFIX -} from "../src/redis.js"; + DEFAULT_USER_SESSIONS_PREFIX, + redisSessionAdapter +} from "../src/drivers/redis.js"; import type { QueryHandler } from "@lucia-auth/adapter-test"; import type { SessionSchema } from "lucia"; @@ -23,6 +23,8 @@ const redisClient = createClient({ } }); +redisClient.on("error", (err) => console.log("Redis Client Error", err)); + const sessionKey = (sessionId: string) => { return [DEFAULT_SESSION_PREFIX, sessionId].join(":"); }; diff --git a/packages/adapter-session-redis/test/upstash.ts b/packages/adapter-session-redis/test/upstash.ts new file mode 100644 index 000000000..d3dabb6ae --- /dev/null +++ b/packages/adapter-session-redis/test/upstash.ts @@ -0,0 +1,63 @@ +import { Database, testSessionAdapter } from "@lucia-auth/adapter-test"; +import { Redis } from "@upstash/redis"; +import dotenv from "dotenv"; +import { LuciaError } from "lucia"; +import { resolve } from "path"; + +import { + DEFAULT_SESSION_PREFIX, + DEFAULT_USER_SESSIONS_PREFIX, + upstashSessionAdapter +} from "../src/drivers/upstash"; + +import type { QueryHandler } from "@lucia-auth/adapter-test"; +import type { SessionSchema } from "lucia"; + +dotenv.config({ + path: `${resolve()}/.env` +}); + +const url = process.env.URL; +const token = process.env.TOKEN; + +if (!url || !token) throw new Error(".env is not set up"); + +const upstashClient = new Redis({ + url, + token +}); + +const sessionKey = (sessionId: string) => { + return [DEFAULT_SESSION_PREFIX, sessionId].join(":"); +}; +const userSessionsKey = (userId: string) => { + return [DEFAULT_USER_SESSIONS_PREFIX, userId].join(":"); +}; + +const adapter = upstashSessionAdapter(upstashClient)(LuciaError); + +const queryHandler: QueryHandler = { + session: { + get: async () => { + const keys = await upstashClient.keys(sessionKey("*")); + + const pipeline = upstashClient.pipeline(); + keys.forEach((key) => pipeline.get(key)); + const sessions = pipeline.exec(); + return sessions; + }, + insert: async (session) => { + const pipeline = upstashClient.pipeline(); + pipeline.set(sessionKey(session.id), JSON.stringify(session)); + pipeline.sadd(userSessionsKey(session.user_id), session.id); + await pipeline.exec(); + }, + clear: async () => { + await upstashClient.flushall(); + } + } +}; + +await testSessionAdapter(adapter, new Database(queryHandler)); + +process.exit(0);