diff --git a/.auri/$160srass.md b/.auri/$160srass.md new file mode 100644 index 000000000..871132ce2 --- /dev/null +++ b/.auri/$160srass.md @@ -0,0 +1,8 @@ +--- +package: "@lucia-auth/oauth" # package name +type: "minor" # "major", "minor", "patch" +--- + +Require `lucia@^2.0.0` + - Export `useAuth()` + - Remove `provider()` \ No newline at end of file diff --git a/.auri/$1gd2c9ca.md b/.auri/$1gd2c9ca.md new file mode 100644 index 000000000..c7fca6210 --- /dev/null +++ b/.auri/$1gd2c9ca.md @@ -0,0 +1,15 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Update configuration + - Remove `autoDatabaseCleanup` + - Remove `transformDatabaseUser()` (see `transformUserAttributes()`) + - Replace `generateCustomUserId()` with `generateUserId()` + - Replace `hash` with `passwordHash` + - Replace `origin` with `requestOrigins` + - Replace `sessionCookie` with `sessionCookie.attributes` + - Add `sessionCookie.name` for setting session cookie name + - Add `transformUserAttributes()` for defining user attributes (**`userId` is automatically included**) + - Add `transformSessionAttributes()` for defining session attributes \ No newline at end of file diff --git a/.auri/$3i5r5bib.md b/.auri/$3i5r5bib.md new file mode 100644 index 000000000..6e1990ac4 --- /dev/null +++ b/.auri/$3i5r5bib.md @@ -0,0 +1,11 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Update `Auth` methods: + - Remove `getSessionUser()` + - Remove `validateSessionUser()` + - Remove `parseRequestHeaders()` + - Add `readSessionCookie()` + - Add `validateRequestOrigin()` \ No newline at end of file diff --git a/.auri/$4g93h2e1.md b/.auri/$4g93h2e1.md new file mode 100644 index 000000000..0067e4ee8 --- /dev/null +++ b/.auri/$4g93h2e1.md @@ -0,0 +1,8 @@ +--- +package: "lucia" # package name +type: "minor" # "major", "minor", "patch" +--- + +Support bearer tokens + - Add `Auth.readBearerToken()` + - Add `AuthRequest.validateBearerToken()` \ No newline at end of file diff --git a/.auri/$4omphity.md b/.auri/$4omphity.md new file mode 100644 index 000000000..fcd9f02ab --- /dev/null +++ b/.auri/$4omphity.md @@ -0,0 +1,9 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Remove primary keys + - Remove `Key.primary` + - Rename `Auth.createUser()` params `options.primaryKey` to `options.key` + - Remove column `key(primary_key)` \ No newline at end of file diff --git a/.auri/$5gbqip09.md b/.auri/$5gbqip09.md new file mode 100644 index 000000000..b6b35ab43 --- /dev/null +++ b/.auri/$5gbqip09.md @@ -0,0 +1,6 @@ +--- +package: "@lucia-auth/adapter-sqlite" # package name +type: "minor" # "major", "minor", "patch" +--- + +Add option to configure table names in `betterSqlite3()` and `d1()` \ No newline at end of file diff --git a/.auri/$5xnm1vze.md b/.auri/$5xnm1vze.md new file mode 100644 index 000000000..9c631befd --- /dev/null +++ b/.auri/$5xnm1vze.md @@ -0,0 +1,6 @@ +--- +package: "@lucia-auth/adapter-mysql" # package name +type: "minor" # "major", "minor", "patch" +--- + +Add option to configure table names in `mysql2()` and `planetscale()` \ No newline at end of file diff --git a/.auri/$8fe25uuj.md b/.auri/$8fe25uuj.md new file mode 100644 index 000000000..bc261f60c --- /dev/null +++ b/.auri/$8fe25uuj.md @@ -0,0 +1,6 @@ +--- +package: "@lucia-auth/adapter-postgresql" # package name +type: "minor" # "major", "minor", "patch" +--- + +Add option to configure table names in `pg()` \ No newline at end of file diff --git a/.auri/$97z0unep.md b/.auri/$97z0unep.md new file mode 100644 index 000000000..7e5d360d1 --- /dev/null +++ b/.auri/$97z0unep.md @@ -0,0 +1,6 @@ +--- +package: "@lucia-auth/oauth" # package name +type: "major" # "major", "minor", "patch" +--- + +Remove `redirectUri` from `getAuthorizationUrl()` \ No newline at end of file diff --git a/.auri/$awt53nzt.md b/.auri/$awt53nzt.md new file mode 100644 index 000000000..c6fdf2b0e --- /dev/null +++ b/.auri/$awt53nzt.md @@ -0,0 +1,7 @@ +--- +package: "@lucia-auth/adapter-test" # package name +type: "major" # "major", "minor", "patch" +--- + +Require `lucia@^2.0.0` + - Update tests \ No newline at end of file diff --git a/.auri/$buyqgbd1.md b/.auri/$buyqgbd1.md new file mode 100644 index 000000000..bad6d1478 --- /dev/null +++ b/.auri/$buyqgbd1.md @@ -0,0 +1,9 @@ +--- +package: "@lucia-auth/adapter-test" # package name +type: "major" # "major", "minor", "patch" +--- + +Update `testAdapter()` and `testSessionAdapter()` + - Rename type `QueryHandler` to `LuciaQueryHandler` + - Remove `testUserAdapter()` + - Test modules no longer end process by default \ No newline at end of file diff --git a/.auri/$cadjkp1x.md b/.auri/$cadjkp1x.md new file mode 100644 index 000000000..7c6510ba7 --- /dev/null +++ b/.auri/$cadjkp1x.md @@ -0,0 +1,10 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Remove single use keys + - **Lucia v2 no longer supports `@lucia-auth/tokens`** + - Remove `Session.type` + - Update `Auth.createKey()` params + - Remove column `key(expires)` \ No newline at end of file diff --git a/.auri/$cvfrkv1s.md b/.auri/$cvfrkv1s.md new file mode 100644 index 000000000..5e6dd7010 --- /dev/null +++ b/.auri/$cvfrkv1s.md @@ -0,0 +1,8 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Update `Session` + - Remove `Session.userId` + - Add `Session.user` \ No newline at end of file diff --git a/.auri/$ecvf1yna.md b/.auri/$ecvf1yna.md new file mode 100644 index 000000000..8d35d5588 --- /dev/null +++ b/.auri/$ecvf1yna.md @@ -0,0 +1,6 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Remove `AuthRequest.validateUser()` \ No newline at end of file diff --git a/.auri/$eyl1lq81.md b/.auri/$eyl1lq81.md new file mode 100644 index 000000000..c3968318c --- /dev/null +++ b/.auri/$eyl1lq81.md @@ -0,0 +1,7 @@ +--- +package: "@lucia-auth/adapter-session-redis" # package name +type: "major" # "major", "minor", "patch" +--- + +Require `lucia@^2.0.0` + - `redis()` expects az single Redis instance instead of 2 \ No newline at end of file diff --git a/.auri/$fgtw8yla.md b/.auri/$fgtw8yla.md new file mode 100644 index 000000000..1d769b099 --- /dev/null +++ b/.auri/$fgtw8yla.md @@ -0,0 +1,8 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Introduce custom session attributes + - Update `Auth.createSession()` params + - Update behavior of `Auth.renewSession()` to include attributes of old session to renewed session automatically \ No newline at end of file diff --git a/.auri/$fozb3rpo.md b/.auri/$fozb3rpo.md new file mode 100644 index 000000000..2cc79de5b --- /dev/null +++ b/.auri/$fozb3rpo.md @@ -0,0 +1,18 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Overhaul adapter API + - Remove `UserAdapter.updateUserAttributes()` + - Remove `UserAdapter.deleteNonPrimaryKey()` + - Remove `UserAdapter.updateKeyPassword()` + - Remove `Adapter?.getSessionAndUserBySessionId()` + - Update `UserAdapter.setUser()` params + - Remove `UserAdapter.getKey()` params `shouldDataBeDeleted()` + - Add `UserAdapter.updateUser()` + - Add `UserAdapter.deleteKey()` + - Add `UserAdapter.updateKey()` + - Add `SessionAdapter.updateSession()` + - Add `Adapter.getSessionAndUser()` + - Rename type `AdapterFunction` to `InitializeAdapter` \ No newline at end of file diff --git a/.auri/$jey1qi9j.md b/.auri/$jey1qi9j.md new file mode 100644 index 000000000..8f04f1719 --- /dev/null +++ b/.auri/$jey1qi9j.md @@ -0,0 +1,9 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Update adapter specifications + - Insert and update methods do not return anything + - Insert and update methods for sessions and keys may optionally throw a Lucia error on invalid user id + - Insert methods do not throw Lucia errors on duplicate session and user ids \ No newline at end of file diff --git a/.auri/$jhfpjptb.md b/.auri/$jhfpjptb.md new file mode 100644 index 000000000..3d985b57c --- /dev/null +++ b/.auri/$jhfpjptb.md @@ -0,0 +1,9 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Remove errors: + - `AUTH_DUPLICATE_SESSION_ID` + - `AUTO_USER_ID_GENERATION_NOT_SUPPORTED` + - `AUTH_EXPIRED_KEY` \ No newline at end of file diff --git a/.auri/$msq2k30f.md b/.auri/$msq2k30f.md new file mode 100644 index 000000000..a3ce54797 --- /dev/null +++ b/.auri/$msq2k30f.md @@ -0,0 +1,6 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Remove auto database clean up functionality \ No newline at end of file diff --git a/.auri/$oba8uk7e.md b/.auri/$oba8uk7e.md new file mode 100644 index 000000000..ad70a6028 --- /dev/null +++ b/.auri/$oba8uk7e.md @@ -0,0 +1,6 @@ +--- +package: "@lucia-auth/adapter-sqlite" # package name +type: "major" # "major", "minor", "patch" +--- + +Require `lucia@^2.0.0` \ No newline at end of file diff --git a/.auri/$osupufo1.md b/.auri/$osupufo1.md new file mode 100644 index 000000000..3b7972859 --- /dev/null +++ b/.auri/$osupufo1.md @@ -0,0 +1,8 @@ +--- +package: "major" # package name +type: "major" # "major", "minor", "patch" +--- + +Update `Lucia` namespace + - Rename `Lucia.UserAttributes` to `Lucia.DatabaseUserAttributes` + - Add `Lucia.DatabaseSessionAttributes` \ No newline at end of file diff --git a/.auri/$oto9r3vz.md b/.auri/$oto9r3vz.md new file mode 100644 index 000000000..52d0cbbdf --- /dev/null +++ b/.auri/$oto9r3vz.md @@ -0,0 +1,6 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Update `Middleware` takes a new `Context` params \ No newline at end of file diff --git a/.auri/$q972t1ak.md b/.auri/$q972t1ak.md new file mode 100644 index 000000000..30e579a34 --- /dev/null +++ b/.auri/$q972t1ak.md @@ -0,0 +1,6 @@ +--- +package: "@lucia-auth/adapter-mysql" # package name +type: "major" # "major", "minor", "patch" +--- + +Require `lucia@^2.0.0` \ No newline at end of file diff --git a/.auri/$qcdnw329.md b/.auri/$qcdnw329.md new file mode 100644 index 000000000..e3aeeceb3 --- /dev/null +++ b/.auri/$qcdnw329.md @@ -0,0 +1,10 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Update exports: + - **Replace default export with named `lucia()`** + - Removed `generateRandomString()` + - Removed `serializeCookie()` + - Removed `Cookie` \ No newline at end of file diff --git a/.auri/$rlqdhur2.md b/.auri/$rlqdhur2.md new file mode 100644 index 000000000..753af4f36 --- /dev/null +++ b/.auri/$rlqdhur2.md @@ -0,0 +1,6 @@ +--- +package: "@lucia-auth/adapter-postgresql" # package name +type: "major" # "major", "minor", "patch" +--- + +Require `lucia@^2.0.0` \ No newline at end of file diff --git a/.auri/$sbxeojc3.md b/.auri/$sbxeojc3.md new file mode 100644 index 000000000..753c67549 --- /dev/null +++ b/.auri/$sbxeojc3.md @@ -0,0 +1,6 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Rename `SESSION_COOKIE_NAME` to `DEFAULT_SESSION_COOKIE_NAME` \ No newline at end of file diff --git a/.auri/$sc875rn3.md b/.auri/$sc875rn3.md new file mode 100644 index 000000000..e7ed40170 --- /dev/null +++ b/.auri/$sc875rn3.md @@ -0,0 +1,9 @@ +--- +package: "lucia" # package name +type: "minor" # "major", "minor", "patch" +--- + +New `lucia/utils` export: + - `generateRandomString()` + - `serializeCookie()` + - `isWithinExpiration()` \ No newline at end of file diff --git a/.auri/$tt1sakae.md b/.auri/$tt1sakae.md new file mode 100644 index 000000000..7e7e09f99 --- /dev/null +++ b/.auri/$tt1sakae.md @@ -0,0 +1,8 @@ +--- +package: "@lucia-auth/adapter-mongoose" # package name +type: "major" # "major", "minor", "patch" +--- + +Require `lucia@^2.0.0` + - Export adapter as named exports (`mongoose()`) + - Update adapter params \ No newline at end of file diff --git a/.auri/$w6gqe7de.md b/.auri/$w6gqe7de.md new file mode 100644 index 000000000..07329ad88 --- /dev/null +++ b/.auri/$w6gqe7de.md @@ -0,0 +1,6 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +**NPM package `lucia-auth` is renamed to `lucia`** \ No newline at end of file diff --git a/.auri/$xwujxvj7.md b/.auri/$xwujxvj7.md new file mode 100644 index 000000000..0174015e2 --- /dev/null +++ b/.auri/$xwujxvj7.md @@ -0,0 +1,8 @@ +--- +package: "@lucia-auth/adapter-prisma" # package name +type: "major" # "major", "minor", "patch" +--- + +Require `lucia@^2.0.0` + - Export adapter as named exports (`prisma()`) + - Update adapter params \ No newline at end of file diff --git a/.auri/$yk3hou53.md b/.auri/$yk3hou53.md new file mode 100644 index 000000000..53508436f --- /dev/null +++ b/.auri/$yk3hou53.md @@ -0,0 +1,8 @@ +--- +package: "lucia" # package name +type: "major" # "major", "minor", "patch" +--- + +Update `RequestContext`: + - Add `RequestContext.headers.authorization` + - Add optional `RequestContext.storedSessionCookie` \ No newline at end of file diff --git a/.auri/release.config.json b/.auri/release.config.json new file mode 100644 index 000000000..f73f6bd53 --- /dev/null +++ b/.auri/release.config.json @@ -0,0 +1,3 @@ +{ + "stage": "beta" +} \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 50b837a23..c03491287 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -6,7 +6,8 @@ module.exports = { "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/ban-types": "off", "@typescript-eslint/no-empty-interface": "off", - "no-async-promise-executor": "off" + "no-async-promise-executor": "off", + "no-useless-catch": "off" }, parser: "@typescript-eslint/parser", extends: [ diff --git a/documentation/content/main/adapters/d1.md b/documentation/content/main/adapters/d1.md index b1453d340..5e182569f 100644 --- a/documentation/content/main/adapters/d1.md +++ b/documentation/content/main/adapters/d1.md @@ -31,7 +31,7 @@ yarn add @lucia-auth/adapter-sqlite Since the database instance is bound to the request, Lucia and the adapter must be initialized on a per-request basis. ```ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import { d1 } from "@lucia-auth/adapter-sqlite"; import type { D1Database } from "@cloudflare/workers-types"; diff --git a/documentation/content/main/adapters/drizzle.md b/documentation/content/main/adapters/drizzle.md index c9a178e97..1f2c21f3f 100644 --- a/documentation/content/main/adapters/drizzle.md +++ b/documentation/content/main/adapters/drizzle.md @@ -71,7 +71,7 @@ Refer to the [`mysql2`](/adapters/mysql#mysql2) section. ```ts import mysql from "mysql2/promise"; import { drizzle } from "drizzle-orm/mysql2"; -import lucia from "lucia-auth"; +import lucia from "lucia"; import { mysql2 } from "@lucia-auth/adapter-mysql"; const connectionPool = mysql.createPool({ @@ -93,7 +93,7 @@ Refer to the [`planetscale`](/adapters/mysql#planetscale) section. ```ts import { connect } from "@planetscale/database"; import { drizzle } from "drizzle-orm/planetscale"; -import lucia from "lucia-auth"; +import lucia from "lucia"; import { planetscale } from "@lucia-auth/adapter-mysql"; const connection = connect({ @@ -163,7 +163,7 @@ Refer to the [`pg`](/adapters/postgresql#pg) section. ```ts import postgres from "pg"; import { drizzle } from "drizzle-orm/node-postgres"; -import lucia from "lucia-auth"; +import lucia from "lucia"; import { pg } from "@lucia-auth/adapter-postgresql"; const connectionPool = new postgres.Pool({ @@ -216,7 +216,7 @@ Refer to the [`better-sqlite3`](/adapters/sqlite#better-sqlite3) section. ```ts import sqlite from "better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3"; -import lucia from "lucia-auth"; +import lucia from "lucia"; import { betterSqlite3 } from "@lucia-auth/adapter-sqlite"; const database = sqlite(pathToDbFile); diff --git a/documentation/content/main/adapters/kysely.md b/documentation/content/main/adapters/kysely.md index 8f507eac9..a9b2b3bb7 100644 --- a/documentation/content/main/adapters/kysely.md +++ b/documentation/content/main/adapters/kysely.md @@ -47,7 +47,7 @@ Refer to the [MySQL](/adapters/mysql) page for the schema and other details. ```ts import mysql from "mysql2/promise"; import { Kysely, MysqlDialect } from "kysely"; -import lucia from "lucia-auth"; +import lucia from "lucia"; import { mysql2 } from "@lucia-auth/adapter-mysql"; const connectionPool = mysql.createPool({ @@ -81,7 +81,7 @@ yarn add @planetscale/database kysely-planetscale import { connect } from "@planetscale/database"; import { Kysely } from "kysely"; import { PlanetScaleDialect } from "kysely-planetscale"; -import { lucia } from "lucia-auth"; +import { lucia } from "lucia"; import { planetscale } from "@lucia-auth/adapter-mysql"; const dbConfig = { @@ -132,7 +132,7 @@ Refer to the [PostgreSQL](/adapters/postgresql) page for the schema and other de ```ts import postgres from "pg"; import { Kysely, PostgresDialect } from "kysely"; -import lucia from "lucia-auth"; +import lucia from "lucia"; import { pg } from "@lucia-auth/adapter-postgresql"; const connectionPool = new postgres.Pool({ @@ -183,7 +183,7 @@ Refer to the [SQLite](/adapters/sqlite) page for the schema and other details. ```ts import sqlite from "better-sqlite3"; import { Kysely, SqliteDialect } from "kysely"; -import lucia from "lucia-auth"; +import lucia from "lucia"; import { betterSqlite3 } from "@lucia-auth/adapter-sqlite"; const database = sqlite(pathToDbFile); diff --git a/documentation/content/main/adapters/redis.md b/documentation/content/main/adapters/redis.md index 71e04755e..336b67dfc 100644 --- a/documentation/content/main/adapters/redis.md +++ b/documentation/content/main/adapters/redis.md @@ -38,7 +38,7 @@ You will need to set up a different adapter for storing users. ```ts // lucia.js -import lucia from "lucia-auth"; +import lucia from "lucia"; import redis from "@lucia-auth/adapter-session-redis"; import prisma from "@lucia-auth/adapter-prisma"; import { createClient } from "redis"; diff --git a/documentation/content/main/basics/error-handling.md b/documentation/content/main/basics/error-handling.md index cc9c88eb5..f7baca6b5 100644 --- a/documentation/content/main/basics/error-handling.md +++ b/documentation/content/main/basics/error-handling.md @@ -9,7 +9,7 @@ Errors are handled by throwing [`LuciaError`](/reference/lucia-auth/luciaerror) Using a try-catch block, the error message can be read like so: ```ts -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { auth } from "./lucia.js"; try { diff --git a/documentation/content/main/basics/handle-requests.astro.md b/documentation/content/main/basics/handle-requests.astro.md index 1399d4385..be8f6dbb9 100644 --- a/documentation/content/main/basics/handle-requests.astro.md +++ b/documentation/content/main/basics/handle-requests.astro.md @@ -45,7 +45,7 @@ The middleware can be configured with the [`middleware`](/basics/configuration#m ```ts import { astro } from "lucia-auth/middleware"; -import lucia from "lucia-auth"; +import lucia from "lucia"; const auth = lucia({ middleware: astro() diff --git a/documentation/content/main/basics/handle-requests.md b/documentation/content/main/basics/handle-requests.md index a7eeffbbf..56f331031 100644 --- a/documentation/content/main/basics/handle-requests.md +++ b/documentation/content/main/basics/handle-requests.md @@ -35,7 +35,7 @@ The middleware can be configured with the [`middleware`](/basics/configuration#m ```ts import { lucia as luciaMiddleware } from "lucia-auth/middleware"; -import lucia from "lucia-auth"; +import lucia from "lucia"; const auth = lucia({ middleware: luciaMiddleware() diff --git a/documentation/content/main/basics/handle-requests.nextjs.md b/documentation/content/main/basics/handle-requests.nextjs.md index 4a7809bb1..29694d937 100644 --- a/documentation/content/main/basics/handle-requests.nextjs.md +++ b/documentation/content/main/basics/handle-requests.nextjs.md @@ -75,7 +75,7 @@ The middleware can be configured with the [`middleware`](/basics/configuration#m ```ts import { nextjs } from "lucia-auth/middleware"; -import lucia from "lucia-auth"; +import lucia from "lucia"; const auth = lucia({ middleware: nextjs() diff --git a/documentation/content/main/basics/handle-requests.nuxt.md b/documentation/content/main/basics/handle-requests.nuxt.md index 285eb2cd3..5090055b9 100644 --- a/documentation/content/main/basics/handle-requests.nuxt.md +++ b/documentation/content/main/basics/handle-requests.nuxt.md @@ -36,7 +36,7 @@ The middleware can be configured with the [`middleware`](/basics/configuration#m ```ts import { h3 } from "lucia-auth/middleware"; -import lucia from "lucia-auth"; +import lucia from "lucia"; const auth = lucia({ middleware: h3() diff --git a/documentation/content/main/basics/handle-requests.qwik.md b/documentation/content/main/basics/handle-requests.qwik.md index 9f2a2c7cd..dbc953cfb 100644 --- a/documentation/content/main/basics/handle-requests.qwik.md +++ b/documentation/content/main/basics/handle-requests.qwik.md @@ -49,7 +49,7 @@ The middleware can be configured with the [`middleware`](/basics/configuration#m ```ts import { qwik } from "lucia-auth/middleware"; -import lucia from "lucia-auth"; +import lucia from "lucia"; const auth = lucia({ middleware: qwik() diff --git a/documentation/content/main/basics/handle-requests.remix.md b/documentation/content/main/basics/handle-requests.remix.md index 6e3773367..d0b9cbd83 100644 --- a/documentation/content/main/basics/handle-requests.remix.md +++ b/documentation/content/main/basics/handle-requests.remix.md @@ -41,7 +41,7 @@ The middleware can be configured with the [`middleware`](/basics/configuration#m ```ts import { web } from "lucia-auth/middleware"; -import lucia from "lucia-auth"; +import lucia from "lucia"; const auth = lucia({ middleware: web() diff --git a/documentation/content/main/basics/handle-requests.sveltekit.md b/documentation/content/main/basics/handle-requests.sveltekit.md index 89fa147ba..2283dd894 100644 --- a/documentation/content/main/basics/handle-requests.sveltekit.md +++ b/documentation/content/main/basics/handle-requests.sveltekit.md @@ -61,7 +61,7 @@ The middleware can be configured with the [`middleware`](/basics/configuration#m ```ts import { sveltekit } from "lucia-auth/middleware"; -import lucia from "lucia-auth"; +import lucia from "lucia"; const auth = lucia({ middleware: sveltekit() diff --git a/documentation/content/main/basics/sessions.md b/documentation/content/main/basics/sessions.md index 79b61fa46..91fd84ef8 100644 --- a/documentation/content/main/basics/sessions.md +++ b/documentation/content/main/basics/sessions.md @@ -96,7 +96,7 @@ try { Alternatively, you can read the cookie directly. The cookie name is provided as a `SESSION_COOKIE_NAME` constant. Make sure to implement your own CSRF protection in this case. ```ts -import { SESSION_COOKIE_NAME } from "lucia-auth"; +import { SESSION_COOKIE_NAME } from "lucia"; import { auth } from "./lucia.js"; const sessionId = getCookie(SESSION_COOKIE_NAME); diff --git a/documentation/content/main/basics/user-attributes.md b/documentation/content/main/basics/user-attributes.md index c946b7608..bf9d11562 100644 --- a/documentation/content/main/basics/user-attributes.md +++ b/documentation/content/main/basics/user-attributes.md @@ -40,7 +40,7 @@ Add the column names and the value type inside `Lucia.UserAttributes`: ```ts // app.d.ts -/// +/// declare namespace Lucia { // ... type UserAttributes = { @@ -89,7 +89,7 @@ This should be typed in `Lucia.UserAttributes`: ```ts // app.d.ts -/// +/// declare namespace Lucia { // ... interface UserAttributes { diff --git a/documentation/content/main/basics/users.md b/documentation/content/main/basics/users.md index a7c23e5e6..8fe7c7b08 100644 --- a/documentation/content/main/basics/users.md +++ b/documentation/content/main/basics/users.md @@ -179,7 +179,7 @@ You can generate your own user ids by setting [`generateCustomUserId()`](/basics If you need to generate a cryptographically random alphanumeric string, Lucia provides [`generateRandomString()`](/reference/lucia-auth/lucia-auth#generaterandomstring). This function is based on the [`nanoid`](https://github.com/ai/nanoid) package. ```ts -import { generateCustomUserId } from "lucia-auth"; +import { generateCustomUserId } from "lucia"; lucia({ generateCustomUserId: () => { diff --git a/documentation/content/main/custom-adapters/testing-adapters.md b/documentation/content/main/custom-adapters/testing-adapters.md index c636a76aa..3d74aecb5 100644 --- a/documentation/content/main/custom-adapters/testing-adapters.md +++ b/documentation/content/main/custom-adapters/testing-adapters.md @@ -86,7 +86,7 @@ Import one of the three testing function and provide both the adapter and query ```ts import { testAdapter } from "@lucia-auth/adapter-test"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; const adapter = adapterKysely()(LuciaError); await testAdapter(adapter, queryHandler); diff --git a/documentation/content/main/nextjs.nextjs/username-password-example-app-router.md b/documentation/content/main/nextjs.nextjs/username-password-example-app-router.md index 469b768a8..3a5d8af1b 100644 --- a/documentation/content/main/nextjs.nextjs/username-password-example-app-router.md +++ b/documentation/content/main/nextjs.nextjs/username-password-example-app-router.md @@ -31,7 +31,7 @@ In `lucia.d.ts`, add `username` in `UserAttributes` since we added `username` co ```ts // lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("$lib/server/lucia.js").Auth; type UserAttributes = { @@ -160,7 +160,7 @@ Users can be created with `createUser()`. This will create a new primary key tha ```ts // app/api/signup/route.ts import { auth } from "@/auth/lucia"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { Prisma } from "@prisma/client"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; @@ -300,7 +300,7 @@ We’ll use the key created in the previous section to reference the user and au import { auth } from "@/auth/lucia"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; export const POST = async (request: Request) => { const { username, password } = await request.json(); diff --git a/documentation/content/main/start-here/getting-started.astro.md b/documentation/content/main/start-here/getting-started.astro.md index 61cb4562a..025d3a73f 100644 --- a/documentation/content/main/start-here/getting-started.astro.md +++ b/documentation/content/main/start-here/getting-started.astro.md @@ -34,7 +34,7 @@ In `src/lib/lucia.ts`, import [`lucia`](/reference/lucia-auth/auth) from `lucia- ```ts // src/lib/lucia.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import prisma from "@lucia-auth/adapter-prisma"; import { PrismaClient } from "@prisma/client"; import { astro } from "lucia-auth/middleware"; @@ -71,7 +71,7 @@ Create `src/lucia.d.ts`, and inside it configure your types. The path in `import ```ts // src/lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("./lib/lucia.js").Auth; type UserAttributes = {}; @@ -84,7 +84,7 @@ declare namespace Lucia { ```ts // auth/lucia.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import "lucia-auth/polyfill/node"; // ... diff --git a/documentation/content/main/start-here/getting-started.md b/documentation/content/main/start-here/getting-started.md index a9df5ca5a..cc85a3561 100644 --- a/documentation/content/main/start-here/getting-started.md +++ b/documentation/content/main/start-here/getting-started.md @@ -44,7 +44,7 @@ In a TypeScript file, import [`lucia`](/reference/lucia-auth/auth) and an adapte ```ts // lucia.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import prisma from "@lucia-auth/adapter-prisma"; import { PrismaClient } from "@prisma/client"; @@ -63,7 +63,7 @@ This module **should NOT be imported from the client**. If you're using Node as is for handling requests, use the [Node middleware](/reference/lucia-auth/middleware#node): ```ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import { node } from "lucia-auth/middleware"; // ... @@ -78,7 +78,7 @@ export const auth = lucia({ If you are using Express for handling requests, use the [Express middleware](/reference/lucia-auth/middleware#express): ```ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import { express } from "lucia-auth/middleware"; // ... @@ -93,7 +93,7 @@ export const auth = lucia({ And, if you're dealing with the standard `Request`/`Response`, use the [Web middleware](/reference/lucia-auth/middleware#web): ```ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import { web } from "lucia-auth/middleware"; // ... @@ -111,7 +111,7 @@ In a TypeScript declaration file (`.d.ts`), declare a `Lucia` namespace. The pat ```ts // app.d.ts -/// +/// declare namespace Lucia { type Auth = import("./lucia.js").Auth; type UserAttributes = {}; @@ -124,7 +124,7 @@ declare namespace Lucia { ```ts // auth/lucia.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import "lucia-auth/polyfill/node"; // ... diff --git a/documentation/content/main/start-here/getting-started.nextjs.md b/documentation/content/main/start-here/getting-started.nextjs.md index d2b41b597..10353722b 100644 --- a/documentation/content/main/start-here/getting-started.nextjs.md +++ b/documentation/content/main/start-here/getting-started.nextjs.md @@ -38,7 +38,7 @@ In `auth/lucia.ts`, import [`lucia`](/reference/lucia-auth/auth) from `lucia-aut ```ts // auth/lucia.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import { nextjs } from "lucia-auth/middleware"; import prisma from "@lucia-auth/adapter-prisma"; import { PrismaClient } from "@prisma/client"; @@ -61,7 +61,7 @@ Create `lucia.d.ts`, and inside it configure your types. The path in `import('./ ```ts // lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("./auth/lucia").Auth; type UserAttributes = {}; @@ -74,7 +74,7 @@ declare namespace Lucia { ```ts // auth/lucia.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import "lucia-auth/polyfill/node"; // ... diff --git a/documentation/content/main/start-here/getting-started.nuxt.md b/documentation/content/main/start-here/getting-started.nuxt.md index 817d957b2..e31847686 100644 --- a/documentation/content/main/start-here/getting-started.nuxt.md +++ b/documentation/content/main/start-here/getting-started.nuxt.md @@ -34,7 +34,7 @@ In `server/utils/auth.ts`, import [`lucia`](/reference/lucia-auth/auth) from `lu ```ts // server/utils/auth.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import { h3 } from "lucia-auth/middleware"; import prisma from "@lucia-auth/adapter-prisma"; import { PrismaClient } from "@prisma/client"; @@ -55,7 +55,7 @@ Create `server/lucia.d.ts`, and inside it configure your types. The path in `imp ```ts // server/lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("./utils/auth.js").Auth; type UserAttributes = { @@ -70,7 +70,7 @@ declare namespace Lucia { ```ts // server/utils/auth.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import "lucia-auth/polyfill/node"; // ... diff --git a/documentation/content/main/start-here/getting-started.qwik.md b/documentation/content/main/start-here/getting-started.qwik.md index 5eb5c7f00..e289650aa 100644 --- a/documentation/content/main/start-here/getting-started.qwik.md +++ b/documentation/content/main/start-here/getting-started.qwik.md @@ -34,7 +34,7 @@ In `$lib/server/lucia.ts`, import [`lucia`](/reference/lucia-auth/auth) from `lu ```ts // src/lib/lucia.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import { qwik } from "lucia-auth/middleware"; import prismaAdapter from "@lucia-auth/adapter-prisma"; import { PrismaClient } from "@prisma/client"; @@ -56,7 +56,7 @@ Create lucia.d.ts, and inside it configure your types. The path in import('./lib ```ts // src/lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("./lib/lucia.js").Auth; type UserAttributes = {}; diff --git a/documentation/content/main/start-here/getting-started.remix.md b/documentation/content/main/start-here/getting-started.remix.md index 507524507..6cefdd3c2 100644 --- a/documentation/content/main/start-here/getting-started.remix.md +++ b/documentation/content/main/start-here/getting-started.remix.md @@ -34,7 +34,7 @@ In `auth/lucia.server.ts`, import [`lucia`](/reference/lucia-auth/auth) from `lu ```ts // auth/lucia.server.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import { web } from "lucia-auth/middleware"; import prisma from "@lucia-auth/adapter-prisma"; import { PrismaClient } from "@prisma/client"; @@ -56,7 +56,7 @@ Create `lucia.d.ts`, and inside it configure your types. The path in `import('./ ```ts // lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("./auth/lucia.server.js").Auth; type UserAttributes = {}; @@ -74,7 +74,7 @@ Lucia is an ESM package and you must define all modules in `serverDependenciesTo module.exports = { // ... serverDependenciesToBundle: [ - "lucia-auth", + "lucia", "lucia-auth/middleware", "@lucia-auth/adapter-prisma", "lucia-auth/polyfill/node", @@ -91,7 +91,7 @@ module.exports = { ```ts // auth/lucia.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import "lucia-auth/polyfill/node"; // ... diff --git a/documentation/content/main/start-here/getting-started.sveltekit.md b/documentation/content/main/start-here/getting-started.sveltekit.md index ead513149..6885f3850 100644 --- a/documentation/content/main/start-here/getting-started.sveltekit.md +++ b/documentation/content/main/start-here/getting-started.sveltekit.md @@ -34,7 +34,7 @@ In `$lib/server/lucia.ts`, import [`lucia`](/reference/lucia-auth/auth) from `lu ```ts // lib/server/lucia.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import { sveltekit } from "lucia-auth/middleware"; import prisma from "@lucia-auth/adapter-prisma"; import { PrismaClient } from "@prisma/client"; @@ -83,12 +83,12 @@ In `src/app.d.ts`, configure your types. The path in `import('$lib/server/lucia. declare global { namespace App { interface Locals { - auth: import("lucia-auth").AuthRequest; + auth: import("lucia").AuthRequest; } } } -/// +/// declare global { namespace Lucia { type Auth = import("$lib/server/lucia").Auth; diff --git a/documentation/content/main/start-here/migrate-to-version-1.md b/documentation/content/main/start-here/migrate-to-version-1.md index 721407367..8002c3a21 100644 --- a/documentation/content/main/start-here/migrate-to-version-1.md +++ b/documentation/content/main/start-here/migrate-to-version-1.md @@ -31,7 +31,7 @@ Middlewares are similar to database adapters but for frameworks, and they are al ```ts import { node } from "lucia-auth/middleware"; -import lucia from "lucia-auth"; +import lucia from "lucia"; export const auth = lucia({ // ... @@ -52,7 +52,7 @@ With this update, `getUser()` and other SvelteKit specific functions are no long ```ts import { sveltekit } from "lucia-auth/middleware"; -import lucia from "lucia-auth"; +import lucia from "lucia"; export const auth = lucia({ // ... @@ -75,7 +75,7 @@ export const handle: Handle = async ({ event, resolve }) => { // app.d.ts /// declare namespace App { - type AuthRequest = import("lucia-auth").AuthRequest; + type AuthRequest = import("lucia").AuthRequest; // Locals must be an interface and not a type // eslint-disable-next-line @typescript-eslint/no-empty-interface interface Locals extends AuthRequest {} @@ -89,7 +89,7 @@ Add Node middleware (**new in v1.0**): ```ts // astro import { node } from "lucia-auth/middleware"; -import lucia from "lucia-auth"; +import lucia from "lucia"; export const auth = lucia({ // ... @@ -110,7 +110,7 @@ Add Astro middleware: ```ts // astro import { astro } from "lucia-auth/middleware"; -import lucia from "lucia-auth"; +import lucia from "lucia"; export const auth = lucia({ // ... @@ -162,7 +162,7 @@ TypeScript should be able to detect most, if not all, of these changes. ```ts // auth/lucia.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; import "lucia-auth/polyfill/node"; // ... diff --git a/documentation/content/main/start-here/username-password.astro.md b/documentation/content/main/start-here/username-password.astro.md index af012a7ab..aba6c2646 100644 --- a/documentation/content/main/start-here/username-password.astro.md +++ b/documentation/content/main/start-here/username-password.astro.md @@ -32,7 +32,7 @@ In `src/lucia.d.ts`, add `username` in `UserAttributes` since we added `username ```ts // src/lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("$lib/server/lucia.js").Auth; type UserAttributes = { diff --git a/documentation/content/main/start-here/username-password.md b/documentation/content/main/start-here/username-password.md index 8a334572c..6155d3ab1 100644 --- a/documentation/content/main/start-here/username-password.md +++ b/documentation/content/main/start-here/username-password.md @@ -22,7 +22,7 @@ In `src/lucia.d.ts`, add `username` in `UserAttributes` since we added `username ```ts // src/lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("$lib/server/lucia.js").Auth; type UserAttributes = { diff --git a/documentation/content/main/start-here/username-password.nextjs.md b/documentation/content/main/start-here/username-password.nextjs.md index d88bd7668..cfa01c9a3 100644 --- a/documentation/content/main/start-here/username-password.nextjs.md +++ b/documentation/content/main/start-here/username-password.nextjs.md @@ -32,7 +32,7 @@ In `lucia.d.ts`, add `username` in `UserAttributes` since we added `username` co ```ts // lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("$lib/server/lucia.js").Auth; type UserAttributes = { @@ -355,7 +355,7 @@ import type { GetServerSidePropsResult, InferGetServerSidePropsType } from "next"; -import type { User } from "lucia-auth"; +import type { User } from "lucia"; export const getServerSideProps = async ( context: GetServerSidePropsContext diff --git a/documentation/content/main/start-here/username-password.nuxt.md b/documentation/content/main/start-here/username-password.nuxt.md index 497976f2a..036a38399 100644 --- a/documentation/content/main/start-here/username-password.nuxt.md +++ b/documentation/content/main/start-here/username-password.nuxt.md @@ -32,7 +32,7 @@ In `server/lucia.d.ts`, add `username` in `UserAttributes` since we added `usern ```ts // server/lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("./utils/auth.js").Auth; type UserAttributes = { @@ -111,7 +111,7 @@ Users can be created with `createUser()`. This will create a new primary key tha ```ts // server/api/signup.post.ts import { Prisma } from "@prisma/client"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; export default defineEventHandler(async (event) => { const { username, password } = (await readBody(event)) ?? {}; @@ -224,7 +224,7 @@ We’ll use the key created in the previous section to reference the user and au ```ts // server/api/login.post.ts import { Prisma } from "@prisma/client"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; export default defineEventHandler(async (event) => { const { username, password } = (await readBody(event)) ?? {}; diff --git a/documentation/content/main/start-here/username-password.qwik.md b/documentation/content/main/start-here/username-password.qwik.md index a3cd3e730..a8986647a 100644 --- a/documentation/content/main/start-here/username-password.qwik.md +++ b/documentation/content/main/start-here/username-password.qwik.md @@ -32,7 +32,7 @@ In `lucia.d.ts`, add `username` in `UserAttributes` since we added `username` co ```ts // src/lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("./lib/lucia.js").Auth; type UserAttributes = { @@ -112,7 +112,7 @@ import { } from "@builder.io/qwik-city"; import { auth } from "~/lib/lucia"; import { Prisma } from "@prisma/client"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; // create an action to handle the form submission export const useSignupAction = routeAction$( @@ -224,7 +224,7 @@ import { } from "@builder.io/qwik-city"; import { auth } from "~/lib/lucia"; import { Prisma } from "@prisma/client"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; export const useUserLoader = routeLoader$(async (event) => { const authRequest = auth.handleRequest(event); @@ -289,7 +289,7 @@ import { } from "@builder.io/qwik-city"; import { auth } from "~/lib/lucia"; import { Prisma } from "@prisma/client"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; // create an action to handle the form submission export const useLoginAction = routeAction$( @@ -347,7 +347,7 @@ import { } from "@builder.io/qwik-city"; import { auth } from "~/lib/lucia"; import { Prisma } from "@prisma/client"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; // .... the action hook shown above @@ -391,7 +391,7 @@ import { routeAction$ } from "@builder.io/qwik-city"; import { auth } from "~/lib/lucia"; -import type { LuciaError } from "lucia-auth"; +import type { LuciaError } from "lucia"; export const useUserLoader = routeLoader$(async (event) => { const authRequest = auth.handleRequest(event); diff --git a/documentation/content/main/start-here/username-password.remix.md b/documentation/content/main/start-here/username-password.remix.md index 6de509865..3e7baf27d 100644 --- a/documentation/content/main/start-here/username-password.remix.md +++ b/documentation/content/main/start-here/username-password.remix.md @@ -32,7 +32,7 @@ In `lucia.d.ts`, add `username` in `UserAttributes` since we added `username` co ```ts // lucia.d.ts -/// +/// declare namespace Lucia { type Auth = import("$lib/server/lucia.js").Auth; type UserAttributes = { @@ -101,7 +101,7 @@ Users can be created with `createUser()`. This will create a new primary key tha // app/routes/signup.tsx import { Form } from "@remix-run/react"; import { auth } from "@auth/lucia.server"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { redirect, json } from "@remix-run/node"; import { Prisma } from "@prisma/client"; @@ -193,7 +193,7 @@ Define a `loader()`. // app/routes/signup.tsx import { Form } from "@remix-run/react"; import { auth } from "@auth/lucia.server"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { redirect, json } from "@remix-run/node"; import type { LoaderArgs, ActionArgs } from "@remix-run/node"; @@ -259,7 +259,7 @@ We’ll use the key created in the previous section to reference the user and au // pages/api/login.ts import { Form } from "@remix-run/react"; import { auth } from "@auth/lucia.server"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { redirect, json } from "@remix-run/node"; import { Prisma } from "@prisma/client"; @@ -319,7 +319,7 @@ If the session exists, redirect authenticated users to the profile page. // app/routes/login.tsx import { Form } from "@remix-run/react"; import { auth } from "@auth/lucia.server"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { redirect, json } from "@remix-run/node"; import type { LoaderArgs, ActionArgs } from "@remix-run/node"; diff --git a/documentation/content/main/start-here/username-password.sveltekit.md b/documentation/content/main/start-here/username-password.sveltekit.md index 9e1329046..74ca562dd 100644 --- a/documentation/content/main/start-here/username-password.sveltekit.md +++ b/documentation/content/main/start-here/username-password.sveltekit.md @@ -32,7 +32,7 @@ In `src/app.d.ts`, add `username` in `UserAttributes` since we added a `username ```ts // src/app.d.ts -/// +/// declare global { namespace Lucia { type Auth = import("$lib/lucia").Auth; diff --git a/documentation/content/reference/lucia-auth/auth.md b/documentation/content/reference/lucia-auth/auth.md index 002447f40..5482912bf 100644 --- a/documentation/content/reference/lucia-auth/auth.md +++ b/documentation/content/reference/lucia-auth/auth.md @@ -230,7 +230,7 @@ const deleteDeadUserSessions: (userId: string) => Promise; #### Example ```ts -import { auth } from "lucia-auth"; +import { auth } from "lucia"; try { await auth.deleteExpiredUserSession(userId); @@ -257,7 +257,7 @@ const deleteKey: (providerId: string, providerUserId: string) => Promise; #### Example ```ts -import { auth } from "lucia-auth"; +import { auth } from "lucia"; try { await auth.deleteKey("username", "user@example.com"); @@ -621,7 +621,7 @@ const invalidateSession: (sessionId: string) => Promise; #### Example ```ts -import { auth } from "lucia-auth"; +import { auth } from "lucia"; try { await auth.invalidateSession(sessionId); @@ -661,7 +661,7 @@ const renewSession: (sessionId: string) => Promise; #### Example ```ts -import { auth } from "lucia-auth"; +import { auth } from "lucia"; try { const renewedSession = await auth.renewSession(session.sessionId); @@ -741,7 +741,7 @@ const updateUserAttributes: ( #### Example ```ts -import { auth } from "lucia-auth"; +import { auth } from "lucia"; try { await auth.updateUserAttributes(userId, { @@ -886,7 +886,7 @@ const validateSession: (sessionId: string) => Promise; #### Example ```ts -import { auth } from "lucia-auth"; +import { auth } from "lucia"; try { const session = await auth.validateSession(sessionId); @@ -932,7 +932,7 @@ const validateSessionUser: ( #### Example ```ts -import { auth } from "lucia-auth"; +import { auth } from "lucia"; try { const { session, user } = await auth.validateSessionUser(sessionId); diff --git a/documentation/content/reference/lucia-auth/lucia-auth.md b/documentation/content/reference/lucia-auth/lucia-auth.md index acb8cb7e6..9269f244d 100644 --- a/documentation/content/reference/lucia-auth/lucia-auth.md +++ b/documentation/content/reference/lucia-auth/lucia-auth.md @@ -6,7 +6,7 @@ title: "Main (/)" These can be imported from `lucia-auth` and should only be used inside a server context. ```ts -import { generateRandomString } from "lucia-auth"; +import { generateRandomString } from "lucia"; ``` For exported types, refer to [Public types](/reference/lucia-auth/types). @@ -62,8 +62,9 @@ const lucia: (config: Configuration) => Auth; This is exported as default: ```ts -import lucia from "lucia-auth"; -import { default as lucia } from "lucia-auth"; +import lucia from "lucia"; +// or +import { default as lucia } from "lucia"; ``` #### Parameter diff --git a/documentation/content/reference/lucia-auth/types.md b/documentation/content/reference/lucia-auth/types.md index d069fd378..f90d4b4a0 100644 --- a/documentation/content/reference/lucia-auth/types.md +++ b/documentation/content/reference/lucia-auth/types.md @@ -6,7 +6,7 @@ _order: 2 These types can be imported from `lucia-auth`: ```ts -import type { Adapter } from "lucia-auth"; +import type { Adapter } from "lucia"; ``` ## `Adapter` @@ -93,9 +93,9 @@ A namespace. ```ts // lucia.d.ts -/// +/// declare namespace Lucia { - type Auth = import("lucia-auth").Auth; + type Auth = import("lucia").Auth; type UserAttributes = {}; } ``` @@ -108,7 +108,7 @@ Should be set to [`Auth`](/reference/lucia-auth/auth). ```ts // lucia.ts -import lucia from "lucia-auth"; +import lucia from "lucia"; const auth = lucia(); export type Auth = typeof auth; diff --git a/documentation/content/tokens/guides/email-verification-links.md b/documentation/content/tokens/guides/email-verification-links.md index bee248305..40f11d71c 100644 --- a/documentation/content/tokens/guides/email-verification-links.md +++ b/documentation/content/tokens/guides/email-verification-links.md @@ -56,7 +56,7 @@ export const auth = lucia({ ```ts // src/app.d.ts -/// +/// declare global { namespace Lucia { type Auth = import("$lib/lucia").Auth; diff --git a/documentation/content/tokens/guides/email-verification-links.sveltekit.md b/documentation/content/tokens/guides/email-verification-links.sveltekit.md index 7029dd7c2..44fba95cd 100644 --- a/documentation/content/tokens/guides/email-verification-links.sveltekit.md +++ b/documentation/content/tokens/guides/email-verification-links.sveltekit.md @@ -55,7 +55,7 @@ export const auth = lucia({ ```ts // src/app.d.ts -/// +/// declare global { namespace Lucia { type Auth = import("$lib/lucia").Auth; diff --git a/examples/astro-email/package.json b/examples/astro-email/package.json index b4d3a6a19..b96a33729 100644 --- a/examples/astro-email/package.json +++ b/examples/astro-email/package.json @@ -16,7 +16,7 @@ "@lucia-auth/tokens": "latest", "@prisma/client": "^4.12.0", "astro": "^2.1.3", - "lucia-auth": "latest", + "lucia": "latest", "tailwindcss": "^3.0.24" }, "devDependencies": { diff --git a/examples/astro-email/src/auth/email.ts b/examples/astro-email/src/auth/email.ts index 4df4c8704..75d5d38ab 100644 --- a/examples/astro-email/src/auth/email.ts +++ b/examples/astro-email/src/auth/email.ts @@ -1,5 +1,5 @@ import { prismaClient } from "src/db"; -import { generateRandomString } from "lucia-auth"; +import { generateRandomString } from "lucia"; import type { Email as DatabaseEmail } from "@prisma/client"; const sendEmail = async ( diff --git a/examples/astro-email/src/auth/lucia.ts b/examples/astro-email/src/auth/lucia.ts index 6c1d4514a..a11423dc3 100644 --- a/examples/astro-email/src/auth/lucia.ts +++ b/examples/astro-email/src/auth/lucia.ts @@ -1,4 +1,4 @@ -import lucia from "lucia-auth"; +import lucia from "lucia"; import prisma from "@lucia-auth/adapter-prisma"; import { astro } from "lucia-auth/middleware"; import { idToken } from "@lucia-auth/tokens"; diff --git a/examples/astro-email/src/lucia.d.ts b/examples/astro-email/src/lucia.d.ts index 2b3f51d7f..fb1ff8834 100644 --- a/examples/astro-email/src/lucia.d.ts +++ b/examples/astro-email/src/lucia.d.ts @@ -1,4 +1,4 @@ -/// +/// declare namespace Lucia { type Auth = import("@auth/lucia").Auth; type UserAttributes = Omit; diff --git a/examples/astro-email/src/pages/login.astro b/examples/astro-email/src/pages/login.astro index 382260575..6c53900fa 100644 --- a/examples/astro-email/src/pages/login.astro +++ b/examples/astro-email/src/pages/login.astro @@ -1,6 +1,6 @@ --- import { auth } from "@auth/lucia"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { emailRegex, isValidFormSubmission } from "src/forms/submission"; import MainLayout from "src/layouts/MainLayout.astro"; diff --git a/examples/astro-email/src/pages/signup.astro b/examples/astro-email/src/pages/signup.astro index f16828eb8..981f9b48c 100644 --- a/examples/astro-email/src/pages/signup.astro +++ b/examples/astro-email/src/pages/signup.astro @@ -2,7 +2,7 @@ import { sendEmailVerificationEmail } from "@auth/email"; import { auth, emailVerificationToken } from "@auth/lucia"; import { Prisma } from "@prisma/client"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { emailRegex, isValidFormSubmission } from "src/forms/submission"; import MainLayout from "src/layouts/MainLayout.astro"; diff --git a/examples/astro/package.json b/examples/astro/package.json index 4fa757c03..2a0e39236 100644 --- a/examples/astro/package.json +++ b/examples/astro/package.json @@ -17,7 +17,7 @@ "@lucia-auth/oauth": "latest", "@prisma/client": "^4.7.0", "astro": "^1.6.0", - "lucia-auth": "latest", + "lucia": "latest", "svelte": "^3.46.4", "tailwindcss": "^3.0.24" }, diff --git a/examples/astro/src/lib/lucia.ts b/examples/astro/src/lib/lucia.ts index e038b46ce..c2a3bdc5f 100644 --- a/examples/astro/src/lib/lucia.ts +++ b/examples/astro/src/lib/lucia.ts @@ -1,4 +1,4 @@ -import lucia from "lucia-auth"; +import lucia from "lucia"; import "lucia-auth/polyfill/node"; import { astro } from "lucia-auth/middleware"; import prisma from "@lucia-auth/adapter-prisma"; diff --git a/examples/astro/src/lucia.d.ts b/examples/astro/src/lucia.d.ts index 0d87ba134..6e862713e 100644 --- a/examples/astro/src/lucia.d.ts +++ b/examples/astro/src/lucia.d.ts @@ -1,4 +1,4 @@ -/// +/// declare namespace Lucia { type Auth = import("./lib/lucia").Auth; type UserAttributes = { diff --git a/examples/astro/src/pages/login.astro b/examples/astro/src/pages/login.astro index 4c7f06d67..7f228bc81 100644 --- a/examples/astro/src/pages/login.astro +++ b/examples/astro/src/pages/login.astro @@ -1,7 +1,7 @@ --- import MainLayout from "../layouts/MainLayout.astro"; import { auth } from "../lib/lucia"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; const authRequest = auth.handleRequest(Astro); const { session } = await authRequest.validateUser(); diff --git a/examples/astro/src/pages/signup.astro b/examples/astro/src/pages/signup.astro index 58086f88a..2b8179530 100644 --- a/examples/astro/src/pages/signup.astro +++ b/examples/astro/src/pages/signup.astro @@ -3,7 +3,7 @@ import { auth } from "../lib/lucia"; import MainLayout from "../layouts/MainLayout.astro"; import { Prisma } from "@prisma/client"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; const authRequest = auth.handleRequest(Astro); const { session } = await authRequest.validateUser(); diff --git a/examples/nextjs-app/app/api/login/route.ts b/examples/nextjs-app/app/api/login/route.ts index cecb91d6a..6c27a522d 100644 --- a/examples/nextjs-app/app/api/login/route.ts +++ b/examples/nextjs-app/app/api/login/route.ts @@ -1,7 +1,7 @@ import { auth } from "@/auth/lucia"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; export const POST = async (request: Request) => { const { username, password } = (await request.json()) as Partial<{ diff --git a/examples/nextjs-app/app/api/signup/route.ts b/examples/nextjs-app/app/api/signup/route.ts index 67fca2992..55d365802 100644 --- a/examples/nextjs-app/app/api/signup/route.ts +++ b/examples/nextjs-app/app/api/signup/route.ts @@ -1,5 +1,5 @@ import { auth } from "@/auth/lucia"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { Prisma } from "@prisma/client"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; diff --git a/examples/nextjs-app/auth/lucia.ts b/examples/nextjs-app/auth/lucia.ts index 5dc90f397..e24b34b77 100644 --- a/examples/nextjs-app/auth/lucia.ts +++ b/examples/nextjs-app/auth/lucia.ts @@ -1,4 +1,4 @@ -import lucia from "lucia-auth"; +import lucia from "lucia"; import { nextjs } from "lucia-auth/middleware"; import prisma from "@lucia-auth/adapter-prisma"; import { PrismaClient } from "@prisma/client"; diff --git a/examples/nextjs-app/lucia.d.ts b/examples/nextjs-app/lucia.d.ts index 2dcc45d78..ee2987842 100644 --- a/examples/nextjs-app/lucia.d.ts +++ b/examples/nextjs-app/lucia.d.ts @@ -1,4 +1,4 @@ -/// +/// declare namespace Lucia { type Auth = import("./auth/lucia").Auth; type UserAttributes = { diff --git a/examples/nextjs-app/package.json b/examples/nextjs-app/package.json index a4e747132..c903db41e 100644 --- a/examples/nextjs-app/package.json +++ b/examples/nextjs-app/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "lucia-auth": "latest", + "lucia": "latest", "@lucia-auth/adapter-prisma": "latest", "@lucia-auth/oauth": "latest", "@prisma/client": "^4.7.0", diff --git a/examples/nextjs/auth/lucia.ts b/examples/nextjs/auth/lucia.ts index 5dc90f397..e24b34b77 100644 --- a/examples/nextjs/auth/lucia.ts +++ b/examples/nextjs/auth/lucia.ts @@ -1,4 +1,4 @@ -import lucia from "lucia-auth"; +import lucia from "lucia"; import { nextjs } from "lucia-auth/middleware"; import prisma from "@lucia-auth/adapter-prisma"; import { PrismaClient } from "@prisma/client"; diff --git a/examples/nextjs/lucia.d.ts b/examples/nextjs/lucia.d.ts index 0d87ba134..6e862713e 100644 --- a/examples/nextjs/lucia.d.ts +++ b/examples/nextjs/lucia.d.ts @@ -1,4 +1,4 @@ -/// +/// declare namespace Lucia { type Auth = import("./lib/lucia").Auth; type UserAttributes = { diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index d43484284..a395ad83e 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -18,7 +18,7 @@ "encoding": "^0.1.13", "eslint": "8.26.0", "eslint-config-next": "13.0.1", - "lucia-auth": "latest", + "lucia": "latest", "next": "13.0.1", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/examples/nextjs/pages/api/login.ts b/examples/nextjs/pages/api/login.ts index 2ce5cde5a..794b02e50 100644 --- a/examples/nextjs/pages/api/login.ts +++ b/examples/nextjs/pages/api/login.ts @@ -1,7 +1,7 @@ import { auth } from "../../auth/lucia"; import type { NextApiRequest, NextApiResponse } from "next"; -import type { LuciaError } from "lucia-auth"; +import type { LuciaError } from "lucia"; type Data = { error?: string; diff --git a/examples/nextjs/pages/api/signup.ts b/examples/nextjs/pages/api/signup.ts index a6b78ff04..b50643100 100644 --- a/examples/nextjs/pages/api/signup.ts +++ b/examples/nextjs/pages/api/signup.ts @@ -1,5 +1,5 @@ import { auth } from "../../auth/lucia"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { Prisma } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; diff --git a/examples/nextjs/pages/index.tsx b/examples/nextjs/pages/index.tsx index 3813704dc..a6f44274f 100644 --- a/examples/nextjs/pages/index.tsx +++ b/examples/nextjs/pages/index.tsx @@ -6,7 +6,7 @@ import type { GetServerSidePropsResult, InferGetServerSidePropsType } from "next"; -import type { User } from "lucia-auth"; +import type { User } from "lucia"; export const getServerSideProps = async ( context: GetServerSidePropsContext diff --git a/examples/nuxt/package.json b/examples/nuxt/package.json index 52bf39f1b..d144e6544 100644 --- a/examples/nuxt/package.json +++ b/examples/nuxt/package.json @@ -9,7 +9,7 @@ "postinstall": "nuxt prepare" }, "devDependencies": { - "lucia-auth": "latest", + "lucia": "latest", "@lucia-auth/adapter-prisma": "latest", "@lucia-auth/oauth": "latest", "@nuxtjs/tailwindcss": "^6.7.0", diff --git a/examples/nuxt/server/api/login.post.ts b/examples/nuxt/server/api/login.post.ts index be4c0be3e..2a8d84b2b 100644 --- a/examples/nuxt/server/api/login.post.ts +++ b/examples/nuxt/server/api/login.post.ts @@ -1,4 +1,4 @@ -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; export default defineEventHandler(async (event) => { const { username, password } = (await readBody(event)) ?? {}; diff --git a/examples/nuxt/server/api/signup.post.ts b/examples/nuxt/server/api/signup.post.ts index 1f8565d37..dddcd50c2 100644 --- a/examples/nuxt/server/api/signup.post.ts +++ b/examples/nuxt/server/api/signup.post.ts @@ -1,5 +1,5 @@ import { Prisma } from "@prisma/client"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; export default defineEventHandler(async (event) => { const { username, password } = (await readBody(event)) ?? {}; diff --git a/examples/nuxt/server/lucia.d.ts b/examples/nuxt/server/lucia.d.ts index 768a60430..6ee497b16 100644 --- a/examples/nuxt/server/lucia.d.ts +++ b/examples/nuxt/server/lucia.d.ts @@ -1,4 +1,4 @@ -/// +/// declare namespace Lucia { type Auth = import("./utils/auth.js").Auth; type UserAttributes = { diff --git a/examples/nuxt/server/utils/auth.ts b/examples/nuxt/server/utils/auth.ts index 7f1f59e7c..56cdc3be3 100644 --- a/examples/nuxt/server/utils/auth.ts +++ b/examples/nuxt/server/utils/auth.ts @@ -1,4 +1,4 @@ -import lucia from "lucia-auth"; +import lucia from "lucia"; import { h3 } from "lucia-auth/middleware"; import prisma from "@lucia-auth/adapter-prisma"; import { PrismaClient } from "@prisma/client"; diff --git a/examples/qwik/package.json b/examples/qwik/package.json index 7247493db..f3400930d 100644 --- a/examples/qwik/package.json +++ b/examples/qwik/package.json @@ -42,6 +42,6 @@ "dependencies": { "@lucia-auth/adapter-prisma": "latest", "@prisma/client": "^4.13.0", - "lucia-auth": "latest" + "lucia": "latest" } } diff --git a/examples/qwik/src/lib/lucia.ts b/examples/qwik/src/lib/lucia.ts index 0463bf794..42fdf6859 100644 --- a/examples/qwik/src/lib/lucia.ts +++ b/examples/qwik/src/lib/lucia.ts @@ -1,5 +1,5 @@ import prismaAdapter from "@lucia-auth/adapter-prisma"; -import lucia from "lucia-auth"; +import lucia from "lucia"; import { qwik } from "lucia-auth/middleware"; import { prisma } from "./prisma"; diff --git a/examples/qwik/src/lucia.d.ts b/examples/qwik/src/lucia.d.ts index b89990856..476d80b8e 100644 --- a/examples/qwik/src/lucia.d.ts +++ b/examples/qwik/src/lucia.d.ts @@ -1,4 +1,4 @@ -/// +/// declare namespace Lucia { type Auth = import("./lib/lucia.js").Auth; type UserAttributes = { diff --git a/examples/qwik/src/routes/login/index.tsx b/examples/qwik/src/routes/login/index.tsx index d44cf04e8..7cca9ab05 100644 --- a/examples/qwik/src/routes/login/index.tsx +++ b/examples/qwik/src/routes/login/index.tsx @@ -8,7 +8,7 @@ import { routeAction$ } from "@builder.io/qwik-city"; import { auth } from "~/lib/lucia"; -import type { LuciaError } from "lucia-auth"; +import type { LuciaError } from "lucia"; export const useUserLoader = routeLoader$(async (event) => { const authRequest = auth.handleRequest(event); diff --git a/examples/qwik/src/routes/signup/index.tsx b/examples/qwik/src/routes/signup/index.tsx index a6889ef07..f5ed7ac7a 100644 --- a/examples/qwik/src/routes/signup/index.tsx +++ b/examples/qwik/src/routes/signup/index.tsx @@ -9,7 +9,7 @@ import { } from "@builder.io/qwik-city"; import { auth } from "~/lib/lucia"; import { Prisma } from "@prisma/client"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; export const useUserLoader = routeLoader$(async (event) => { const authRequest = auth.handleRequest(event); diff --git a/examples/remix/app/routes/login.tsx b/examples/remix/app/routes/login.tsx index 291467e3b..1c09a5926 100644 --- a/examples/remix/app/routes/login.tsx +++ b/examples/remix/app/routes/login.tsx @@ -1,5 +1,5 @@ import { auth } from "@auth/lucia.server"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { Form, useActionData } from "@remix-run/react"; import { redirect, json } from "@remix-run/node"; diff --git a/examples/remix/app/routes/signup.tsx b/examples/remix/app/routes/signup.tsx index 702391d8e..c5fd1d8a4 100644 --- a/examples/remix/app/routes/signup.tsx +++ b/examples/remix/app/routes/signup.tsx @@ -1,5 +1,5 @@ import { auth } from "@auth/lucia.server"; -import { LuciaError } from "lucia-auth"; +import { LuciaError } from "lucia"; import { Form, useActionData } from "@remix-run/react"; import { redirect, json } from "@remix-run/node"; import { Prisma } from "@prisma/client"; diff --git a/examples/remix/auth/lucia.server.ts b/examples/remix/auth/lucia.server.ts index 343871a91..f544ff9d1 100644 --- a/examples/remix/auth/lucia.server.ts +++ b/examples/remix/auth/lucia.server.ts @@ -1,4 +1,4 @@ -import lucia from "lucia-auth"; +import lucia from "lucia"; import { web } from "lucia-auth/middleware"; import prisma from "@lucia-auth/adapter-prisma"; import { PrismaClient } from "@prisma/client"; diff --git a/examples/remix/lucia.d.ts b/examples/remix/lucia.d.ts index 428073af1..1cb4f79dd 100644 --- a/examples/remix/lucia.d.ts +++ b/examples/remix/lucia.d.ts @@ -1,4 +1,4 @@ -/// +/// declare namespace Lucia { type Auth = import("@auth/lucia.server.js").Auth; type UserAttributes = { diff --git a/examples/remix/package.json b/examples/remix/package.json index 975b03647..456a94c84 100644 --- a/examples/remix/package.json +++ b/examples/remix/package.json @@ -16,15 +16,15 @@ "@remix-run/react": "^1.16.1", "@remix-run/serve": "^1.16.1", "isbot": "^3.6.8", - "lucia-auth": "latest", + "lucia": "latest", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@remix-run/dev": "^1.16.1", "@remix-run/eslint-config": "^1.16.1", - "@types/react": "^18.0.35", - "@types/react-dom": "^18.0.11", + "@types/react": "^18.0.24", + "@types/react-dom": "^18.0.8", "eslint": "^8.38.0", "prisma": "^4.7.0", "tailwindcss": "^3.3.2", diff --git a/examples/remix/remix.config.js b/examples/remix/remix.config.js index 676bc4b22..6831ebbf2 100644 --- a/examples/remix/remix.config.js +++ b/examples/remix/remix.config.js @@ -10,7 +10,7 @@ module.exports = { v2_routeConvention: true }, serverDependenciesToBundle: [ - "lucia-auth", + "lucia", "lucia-auth/middleware", "@lucia-auth/adapter-prisma", "@lucia-auth/oauth", diff --git a/examples/sveltekit-email/package.json b/examples/sveltekit-email/package.json index 24159c193..b19c4728f 100644 --- a/examples/sveltekit-email/package.json +++ b/examples/sveltekit-email/package.json @@ -36,6 +36,6 @@ "@lucia-auth/adapter-prisma": "^1.0.0", "@lucia-auth/tokens": "^1.0.0", "@prisma/client": "^4.12.0", - "lucia-auth": "^1.0.0" + "lucia": "^1.0.0" } } diff --git a/examples/sveltekit-email/src/app.d.ts b/examples/sveltekit-email/src/app.d.ts index 4770ac147..a77388211 100644 --- a/examples/sveltekit-email/src/app.d.ts +++ b/examples/sveltekit-email/src/app.d.ts @@ -3,12 +3,12 @@ declare global { namespace App { interface Locals { - auth: import('lucia-auth').AuthRequest; + auth: import('lucia').AuthRequest; } } } -/// +/// declare global { namespace Lucia { type Auth = import('$lib/lucia').Auth; diff --git a/examples/sveltekit-email/src/lib/email.ts b/examples/sveltekit-email/src/lib/email.ts index 00b479046..067ea3209 100644 --- a/examples/sveltekit-email/src/lib/email.ts +++ b/examples/sveltekit-email/src/lib/email.ts @@ -1,6 +1,6 @@ import { prismaClient } from '$lib/db'; import type { Email as DatabaseEmail } from '@prisma/client'; -import { generateRandomString } from 'lucia-auth'; +import { generateRandomString } from 'lucia'; const sendEmail = async (emailAddress: string, subject: string, content: string) => { await prismaClient.email.create({ diff --git a/examples/sveltekit-email/src/lib/lucia.ts b/examples/sveltekit-email/src/lib/lucia.ts index 27e2c01fb..d52c09888 100644 --- a/examples/sveltekit-email/src/lib/lucia.ts +++ b/examples/sveltekit-email/src/lib/lucia.ts @@ -1,4 +1,4 @@ -import lucia from 'lucia-auth'; +import lucia from 'lucia'; import prisma from '@lucia-auth/adapter-prisma'; import { sveltekit } from 'lucia-auth/middleware'; import { idToken } from '@lucia-auth/tokens'; diff --git a/examples/sveltekit-email/src/routes/(main)/login/+page.server.ts b/examples/sveltekit-email/src/routes/(main)/login/+page.server.ts index c65c12763..bd49d7b62 100644 --- a/examples/sveltekit-email/src/routes/(main)/login/+page.server.ts +++ b/examples/sveltekit-email/src/routes/(main)/login/+page.server.ts @@ -1,6 +1,6 @@ import { auth } from '$lib/lucia'; import { emailRegex } from '$lib/form-submission'; -import { LuciaError } from 'lucia-auth'; +import { LuciaError } from 'lucia'; import { fail, redirect } from '@sveltejs/kit'; import type { PageServerLoad, Actions } from './$types'; diff --git a/examples/sveltekit-email/src/routes/(main)/signup/+page.server.ts b/examples/sveltekit-email/src/routes/(main)/signup/+page.server.ts index c43dac2d2..6e0112eb3 100644 --- a/examples/sveltekit-email/src/routes/(main)/signup/+page.server.ts +++ b/examples/sveltekit-email/src/routes/(main)/signup/+page.server.ts @@ -2,7 +2,7 @@ import { emailRegex } from '$lib/form-submission'; import { fail, redirect } from '@sveltejs/kit'; import { auth, emailVerificationToken } from '$lib/lucia'; import { sendEmailVerificationEmail } from '$lib/email'; -import { LuciaError } from 'lucia-auth'; +import { LuciaError } from 'lucia'; import { Prisma } from '@prisma/client'; import type { PageServerLoad, Actions } from './$types'; diff --git a/examples/sveltekit/package.json b/examples/sveltekit/package.json index ea50d003b..00b8ce29d 100644 --- a/examples/sveltekit/package.json +++ b/examples/sveltekit/package.json @@ -30,7 +30,7 @@ "svelte-preprocess": "^4.10.7", "tailwindcss": "^3.1.8", "tslib": "^2.4.0", - "typescript": "5.x", + "typescript": "latest", "vite": "4.x" }, "type": "module", @@ -39,6 +39,6 @@ "@lucia-auth/oauth": "latest", "@prisma/client": "~4.7.0", "devalue": "^4.0.1", - "lucia-auth": "latest" + "lucia": "latest" } } diff --git a/examples/sveltekit/src/app.d.ts b/examples/sveltekit/src/app.d.ts index 3f358efd9..15800e441 100644 --- a/examples/sveltekit/src/app.d.ts +++ b/examples/sveltekit/src/app.d.ts @@ -1,4 +1,4 @@ -/// +/// declare namespace Lucia { type Auth = import('$lib/server/lucia.js').Auth; type UserAttributes = { @@ -9,6 +9,6 @@ declare namespace Lucia { /// declare namespace App { interface Locals { - auth: import('lucia-auth').AuthRequest; + auth: import('lucia').AuthRequest; } } diff --git a/examples/sveltekit/src/lib/server/lucia.ts b/examples/sveltekit/src/lib/server/lucia.ts index 095029f1c..87d6b0f7e 100644 --- a/examples/sveltekit/src/lib/server/lucia.ts +++ b/examples/sveltekit/src/lib/server/lucia.ts @@ -1,5 +1,5 @@ -import lucia from 'lucia-auth'; -import { sveltekit } from 'lucia-auth/middleware'; +import lucia from 'lucia'; +import { sveltekit } from 'lucia/middleware'; import prisma from '@lucia-auth/adapter-prisma'; import { dev } from '$app/environment'; import { PrismaClient } from '@prisma/client'; diff --git a/examples/sveltekit/src/routes/login/+page.server.ts b/examples/sveltekit/src/routes/login/+page.server.ts index fff3c17bb..f544be999 100644 --- a/examples/sveltekit/src/routes/login/+page.server.ts +++ b/examples/sveltekit/src/routes/login/+page.server.ts @@ -2,7 +2,7 @@ import { fail, type Actions } from '@sveltejs/kit'; import { auth } from '$lib/server/lucia'; import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; -import { LuciaError } from 'lucia-auth'; +import { LuciaError } from 'lucia'; export const load: PageServerLoad = async ({ locals }) => { const session = await locals.auth.validate(); diff --git a/examples/sveltekit/src/routes/signup/+page.server.ts b/examples/sveltekit/src/routes/signup/+page.server.ts index 854ce00b5..e0cb87030 100644 --- a/examples/sveltekit/src/routes/signup/+page.server.ts +++ b/examples/sveltekit/src/routes/signup/+page.server.ts @@ -2,7 +2,7 @@ import { auth } from '$lib/server/lucia'; import { fail, type Actions } from '@sveltejs/kit'; import { Prisma } from '@prisma/client'; import { redirect } from '@sveltejs/kit'; -import { LuciaError } from 'lucia-auth'; +import { LuciaError } from 'lucia'; import type { PageServerLoad } from './$types'; export const actions: Actions = { diff --git a/package.json b/package.json index ee25db50c..b378de7cf 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "lucia", "version": "0.0.1", - "description": "monorepo for lucia-auth", + "description": "Authentication, simple and clean", "scripts": { - "ready": "pnpm i && cd packages/lucia-auth && pnpm build && pnpm i && cd ../adapter-test && pnpm build && pnpm i && cd ../integration-oauth && pnpm build && cd ../adapter-prisma && pnpm build && cd ../integration-tokens && pnpm build && cd .. && pnpm install --no-frozen-lockfile", - "publish-setup": "cd packages/lucia-auth && pnpm i --no-frozen-lockfile && pnpm build && cd ../../ && cd packages/adapter-test && pnpm i --no-frozen-lockfile && pnpm build && cd ../../ && pnpm i --no-frozen-lockfile", + "ready": "pnpm i && cd packages/lucia && pnpm build && cd ../adapter-test && pnpm build && cd ../oauth && pnpm build && cd ../adapter-prisma && pnpm build && cd ../../", + "publish-setup": "pnpm i --no-frozen-lockfile && cd packages/lucia && pnpm build && cd ../adapter-test && pnpm build && cd ../../", "format": "pnpm exec prettier --write .", "preinstall": "npx only-allow pnpm", "auri.format": "pnpm format", @@ -21,7 +21,7 @@ "@types/node": "~18.15.13", "@typescript-eslint/eslint-plugin": "^5.59.6", "@typescript-eslint/parser": "^5.59.6", - "auri": "^0.5.4", + "auri": "^0.6.0", "eslint": "^8.40.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-svelte3": "^4.0.0", @@ -29,15 +29,10 @@ "prettier-plugin-svelte": "^2.10.0", "prettier-plugin-tailwindcss": "^0.2.8", "shx": "^0.3.4", - "typescript": "~5.0.4" - }, - "resolutions": { - "lucia-auth": "workspace:*", - "@lucia-auth/adapter-prisma": "workspace:*", - "@lucia-auth/oauth": "workspace:*" + "typescript": "latest" }, "engines": { - "node": "16.x - 20.x", + "node": "20.x", "pnpm": "*" } } diff --git a/packages/adapter-kysely/CHANGELOG.md b/packages/adapter-kysely/CHANGELOG.md deleted file mode 100644 index 14abfbba2..000000000 --- a/packages/adapter-kysely/CHANGELOG.md +++ /dev/null @@ -1,95 +0,0 @@ -# @lucia-auth/adapter-kysely - -## 1.0.1 - -### Patch changes - -- [#463](https://github.com/pilcrowOnPaper/lucia/pull/463) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : Fix `deleteSession()` - -## 1.0.0 - -### Major changes - -- [#443](https://github.com/pilcrowOnPaper/lucia/pull/443) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : Release version 1.0! - -## 0.8.0 - -### Minor changes - -- [#430](https://github.com/pilcrowOnPaper/lucia/pull/430) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : [Breaking] Require `lucia-auth` 0.11.0 - - - Update schema - -## 0.7.1 - -### Patch changes - -- [#424](https://github.com/pilcrowOnPaper/lucia/pull/424) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : - Update dependencies - -## 0.7.0 - -### Minor changes - -- [#398](https://github.com/pilcrowOnPaper/lucia/pull/398) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : Require `lucia-auth@0.9.0` - -## 0.6.3 - -### Patch changes - -- [#392](https://github.com/pilcrowOnPaper/lucia/pull/392) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : Update peer dependency - -## 0.6.2 - -### Patch changes - -- [#388](https://github.com/pilcrowOnPaper/lucia/pull/388) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : remove unnecessary code - -## 0.6.1 - -### Patch changes - -- [#381](https://github.com/pilcrowOnPaper/lucia/pull/381) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : Update links in README and package.json - -## 0.6.0 - -- [Breaking] Require minimum `lucia-auth` 0.7.0 - -## 0.5.0 - -- [Breaking] Require minimum `lucia-auth` 0.6.0 - -## 0.4.1 - -- Fix `lucia-auth` peer dependency - -## 0.4.0 - -- [Breaking] Require minimum `lucia-auth` 0.5.0 - -## 0.3.1 - -- [Fix] Proper type checking #297 - -- Support `kysely@0.23.0` - -- Move `kysely` to dev dependencies - -## 0.3.0 - -- [Breaking] Require `dialect` parameter - -- Support MySQL - -- Support SQLite - -- Export type `KyselyLuciaDatabase`, `KyselyUser`, `KyselySession` - -## 0.2.0 - -- [Breaking] Require minimum `lucia-auth` 0.4.0 - -- [Breaking] Remove global error handler - -## 0.1.1 - -- Update peer dependency diff --git a/packages/adapter-kysely/README.md b/packages/adapter-kysely/README.md deleted file mode 100644 index a6ca0ee6a..000000000 --- a/packages/adapter-kysely/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `@lucia-auth/adapter-kysely` - -This has been deprecated in favor of the [MySQL adapter](https://lucia-auth.com/database/mysql), [PostgreSQL adapter](https://lucia-auth.com/database/postgresql), and [SQLite adapter](https://lucia-auth.com/database/sqlite). Please refer to [Kysely](https://lucia-auth.com/database/kysely) to learn how to use these adapters with Kyseky. diff --git a/packages/adapter-mongoose/.env.example b/packages/adapter-mongoose/.env.example new file mode 100644 index 000000000..cc3e1ef61 --- /dev/null +++ b/packages/adapter-mongoose/.env.example @@ -0,0 +1 @@ +MONGODB_URL="" \ No newline at end of file diff --git a/packages/adapter-mongoose/.gitignore b/packages/adapter-mongoose/.gitignore index e30934148..7a0ae98eb 100644 --- a/packages/adapter-mongoose/.gitignore +++ b/packages/adapter-mongoose/.gitignore @@ -1,4 +1,5 @@ /node_modules /dist .DS_Store -.env \ No newline at end of file +.env +*.tgz \ No newline at end of file diff --git a/packages/adapter-mongoose/.npmignore b/packages/adapter-mongoose/.npmignore deleted file mode 100644 index 3c3629e64..000000000 --- a/packages/adapter-mongoose/.npmignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/packages/adapter-mongoose/package.json b/packages/adapter-mongoose/package.json index eb60efa6a..3c5762660 100644 --- a/packages/adapter-mongoose/package.json +++ b/packages/adapter-mongoose/package.json @@ -2,21 +2,22 @@ "name": "@lucia-auth/adapter-mongoose", "version": "2.0.0", "description": "Mongoose (MongoDB) adapter for Lucia", - "main": "index.js", - "types": "index.d.ts", - "module": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "module": "dist/index.js", "type": "module", "files": [ - "**/*" + "/dist/", + "CHANGELOG.md" ], "scripts": { - "build": "shx rm -rf ./dist/* && tsc && shx cp ./package.json ./dist && shx cp ./README.md ./dist && shx cp .npmignore dist", + "build": "shx rm -rf ./dist/* && tsc", "test": "tsx test/index.ts", - "auri.publish": "pnpm build && cd dist && pnpm install --no-frozen-lockfile && pnpm publish --no-git-checks --access public && cd ../" + "auri.publish": "pnpm build && pnpm publish --no-git-checks --access public" }, "keywords": [ "lucia", - "lucia-auth", + "lucia", "auth", "authentication", "adapter", @@ -32,17 +33,17 @@ "author": "pilcrowonpaper", "license": "MIT", "exports": { - ".": "./index.js" + ".": "./dist/index.js" }, "peerDependencies": { - "lucia-auth": "^1.3.0", + "lucia": "^2.0.0", "mongoose": "6.x - 7.x" }, "devDependencies": { - "@lucia-auth/adapter-test": "workspace:*", + "@lucia-auth/adapter-test": "latest", "dotenv": "^16.0.3", "tsx": "^3.12.6", "mongoose": "^6.6.1", - "lucia-auth": "workspace:*" + "lucia": "latest" } } diff --git a/packages/adapter-mongoose/src/docs.ts b/packages/adapter-mongoose/src/docs.ts index 2dcae0be0..bb199f31e 100644 --- a/packages/adapter-mongoose/src/docs.ts +++ b/packages/adapter-mongoose/src/docs.ts @@ -1,10 +1,12 @@ +import type { + GlobalDatabaseUserAttributes, + GlobalDatabaseSessionAttributes +} from "lucia"; + export type UserDoc = { _id: string; __v?: any; - _doc?: any; - $__?: any; - username: string; -}; +} & GlobalDatabaseUserAttributes; export type SessionDoc = { _id: string; @@ -12,13 +14,11 @@ export type SessionDoc = { active_expires: number; user_id: string; idle_expires: number; -}; +} & GlobalDatabaseSessionAttributes; export type KeyDoc = { _id: string; __v?: any; user_id: string; - hashed_password?: string | null; - primary_key: boolean; - expires?: number | null; + hashed_password?: string; }; diff --git a/packages/adapter-mongoose/src/index.ts b/packages/adapter-mongoose/src/index.ts index 218797177..1c21fefc6 100644 --- a/packages/adapter-mongoose/src/index.ts +++ b/packages/adapter-mongoose/src/index.ts @@ -1,199 +1 @@ -import type { Mongoose } from "mongoose"; -import { - transformKeyDoc, - transformSessionDoc, - transformUserDoc -} from "./utils.js"; -import type { Adapter, AdapterFunction } from "lucia-auth"; -import type { UserDoc, SessionDoc, KeyDoc } from "./docs.js"; - -const createMongoValues = (object: Record) => { - return Object.fromEntries( - Object.entries(object).map(([key, value]) => { - if (key === "id") return ["_id", value]; - return [key, value]; - }) - ); -}; - -const DEFAULT_PROJECTION = { - $__: 0, - __v: 0, - _doc: 0 -}; - -const adapter = (mongoose: Mongoose): AdapterFunction => { - const User = mongoose.model("auth_user"); - const Session = mongoose.model("auth_session"); - const Key = mongoose.model("auth_key"); - return (LuciaError) => { - return { - getUser: async (userId: string) => { - const userDoc = await User.findById(userId, DEFAULT_PROJECTION).lean(); - if (!userDoc) return null; - return transformUserDoc(userDoc); - }, - getSessionAndUserBySessionId: async (sessionId) => { - const session = await Session.findById( - sessionId, - DEFAULT_PROJECTION - ).lean(); - if (!session) return null; - const user = await User.findById( - session.user_id, - DEFAULT_PROJECTION - ).lean(); - if (!user) return null; - return { - user: transformUserDoc(user), - session: transformSessionDoc(session) - }; - }, - getSession: async (sessionId) => { - const session = await Session.findById( - sessionId, - DEFAULT_PROJECTION - ).lean(); - if (!session) return null; - return transformSessionDoc(session); - }, - getSessionsByUserId: async (userId) => { - const sessions = await Session.find( - { - user_id: userId - }, - DEFAULT_PROJECTION - ).lean(); - return sessions.map((val) => transformSessionDoc(val)); - }, - setUser: async (userId, userAttributes, key) => { - if (key) { - const refKeyDoc = await Key.findById(key.id, DEFAULT_PROJECTION); - if (refKeyDoc) throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - const userDoc = new User( - createMongoValues({ - id: userId, - ...userAttributes - }) - ); - await userDoc.save(); - try { - if (key) { - const keyDoc = new Key(createMongoValues(key)); - await keyDoc.save(); - } - return transformUserDoc(userDoc.toObject()); - } catch (error) { - await Key.findByIdAndDelete(userId); - if ( - error instanceof Error && - error.message.includes("E11000") && - error.message.includes("id") - ) { - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - throw error; - } - }, - deleteUser: async (userId: string) => { - await User.findOneAndDelete({ - _id: userId - }); - }, - setSession: async (session) => { - const userDoc = await User.findById( - session.user_id, - DEFAULT_PROJECTION - ).lean(); - if (!userDoc) throw new LuciaError("AUTH_INVALID_USER_ID"); - try { - const sessionDoc = new Session(createMongoValues(session)); - await Session.create(sessionDoc); - } catch (error) { - if ( - error instanceof Error && - error.message.includes("E11000") && - error.message.includes("id") - ) - throw new LuciaError("AUTH_DUPLICATE_SESSION_ID"); - throw error; - } - }, - deleteSession: async (sessionId) => { - await Session.findByIdAndDelete(sessionId); - }, - deleteSessionsByUserId: async (userId) => { - await Session.deleteMany({ - user_id: userId - }); - }, - updateUserAttributes: async (userId, attributes) => { - const userDoc = await User.findByIdAndUpdate(userId, attributes, { - new: true, - projection: DEFAULT_PROJECTION - }).lean(); - if (!userDoc) throw new LuciaError("AUTH_INVALID_USER_ID"); - return transformUserDoc(userDoc); - }, - getKey: async (keyId) => { - const keyDoc = await Key.findById(keyId, DEFAULT_PROJECTION).lean(); - if (!keyDoc) return null; - const transformedKeyData = transformKeyDoc(keyDoc); - return transformedKeyData; - }, - setKey: async (key) => { - const userDoc = await User.findById(key.user_id, DEFAULT_PROJECTION); - if (!userDoc) throw new LuciaError("AUTH_INVALID_USER_ID"); - try { - const keyDoc = new Key(createMongoValues(key)); - await Key.create(keyDoc); - } catch (error) { - if ( - error instanceof Error && - error.message.includes("E11000") && - error.message.includes("id") - ) - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - throw error; - } - }, - getKeysByUserId: async (userId) => { - const keyDocs = await Key.find( - { - user_id: userId - }, - DEFAULT_PROJECTION - ).lean(); - return keyDocs.map((val) => transformKeyDoc(val)); - }, - updateKeyPassword: async (key, hashedPassword) => { - const keyDoc = await Key.findByIdAndUpdate( - key, - { - hashed_password: hashedPassword - }, - { - new: true, - projection: DEFAULT_PROJECTION - } - ).lean(); - if (!keyDoc) throw new LuciaError("AUTH_INVALID_KEY_ID"); - return transformKeyDoc(keyDoc); - }, - deleteKeysByUserId: async (userId) => { - await Key.deleteMany({ - user_id: userId - }); - }, - deleteNonPrimaryKey: async (keyId) => { - await Key.deleteOne({ - _id: keyId, - primary_key: false - }); - } - }; - }; -}; - -export default adapter; +export { mongooseAdapter as mongoose } from "./mongoose.js"; diff --git a/packages/adapter-mongoose/src/lucia.d.ts b/packages/adapter-mongoose/src/lucia.d.ts index f61de1891..8026ca988 100644 --- a/packages/adapter-mongoose/src/lucia.d.ts +++ b/packages/adapter-mongoose/src/lucia.d.ts @@ -1,5 +1,6 @@ -/// +/// declare namespace Lucia { type Auth = any; - type UserAttributes = {}; + type DatabaseUserAttributes = any; + type DatabaseSessionAttributes = any; } diff --git a/packages/adapter-mongoose/src/mongoose.ts b/packages/adapter-mongoose/src/mongoose.ts new file mode 100644 index 000000000..2def0ec16 --- /dev/null +++ b/packages/adapter-mongoose/src/mongoose.ts @@ -0,0 +1,179 @@ +import type { Model } from "mongoose"; +import type { + Adapter, + InitializeAdapter, + KeySchema, + UserSchema, + SessionSchema +} from "lucia"; +import type { UserDoc, SessionDoc, KeyDoc } from "./docs.js"; + +export const DEFAULT_PROJECTION = { + $__: 0, + __v: 0, + _doc: 0 +}; + +export const mongooseAdapter = (models: { + User: Model; + Session: Model; + Key: Model; +}): InitializeAdapter => { + const { User, Session, Key } = models; + return (LuciaError) => { + return { + getUser: async (userId: string) => { + const userDoc = await User.findById(userId, DEFAULT_PROJECTION).lean(); + if (!userDoc) return null; + return transformUserDoc(userDoc); + }, + setUser: async (user, key) => { + if (key) { + const refKeyDoc = await Key.findById(key.id, DEFAULT_PROJECTION); + if (refKeyDoc) throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + const userDoc = new User(createMongoValues(user)); + await userDoc.save(); + if (!key) return; + try { + const keyDoc = new Key(createMongoValues(key)); + await keyDoc.save(); + } catch (error) { + await Key.findByIdAndDelete(user.id); + if ( + error instanceof Error && + error.message.includes("E11000") && + error.message.includes("id") + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw error; + } + }, + deleteUser: async (userId: string) => { + await User.findByIdAndDelete(userId); + }, + updateUser: async (userId, partialUser) => { + await User.findByIdAndUpdate(userId, partialUser, { + new: true, + projection: DEFAULT_PROJECTION + }).lean(); + }, + + getSession: async (sessionId) => { + const session = await Session.findById( + sessionId, + DEFAULT_PROJECTION + ).lean(); + if (!session) return null; + return transformSessionDoc(session); + }, + getSessionsByUserId: async (userId) => { + const sessions = await Session.find( + { + user_id: userId + }, + DEFAULT_PROJECTION + ).lean(); + return sessions.map((val) => transformSessionDoc(val)); + }, + setSession: async (session) => { + const sessionDoc = new Session(createMongoValues(session)); + await sessionDoc.save(); + }, + deleteSession: async (sessionId) => { + await Session.findByIdAndDelete(sessionId); + }, + deleteSessionsByUserId: async (userId) => { + await Session.deleteMany({ + user_id: userId + }); + }, + updateSession: async (sessionId, partialUser) => { + await Session.findByIdAndUpdate(sessionId, partialUser, { + new: true, + projection: DEFAULT_PROJECTION + }).lean(); + }, + + getKey: async (keyId) => { + const keyDoc = await Key.findById(keyId, DEFAULT_PROJECTION).lean(); + if (!keyDoc) return null; + return transformKeyDoc(keyDoc); + }, + setKey: async (key) => { + try { + const keyDoc = new Key(createMongoValues(key)); + await Key.create(keyDoc); + } catch (error) { + if ( + error instanceof Error && + error.message.includes("E11000") && + error.message.includes("id") + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw error; + } + }, + getKeysByUserId: async (userId) => { + const keyDocs = await Key.find( + { + user_id: userId + }, + DEFAULT_PROJECTION + ).lean(); + return keyDocs.map((val) => transformKeyDoc(val)); + }, + deleteKey: async (keyId) => { + await Key.findByIdAndDelete(keyId); + }, + deleteKeysByUserId: async (userId) => { + await Key.deleteMany({ + user_id: userId + }); + }, + updateKey: async (keyId, partialKey) => { + await Key.findByIdAndUpdate(keyId, partialKey, { + new: true, + projection: DEFAULT_PROJECTION + }).lean(); + } + }; + }; +}; + +export const createMongoValues = (object: Record) => { + return Object.fromEntries( + Object.entries(object).map(([key, value]) => { + if (key === "id") return ["_id", value]; + return [key, value]; + }) + ); +}; + +export const transformUserDoc = (row: UserDoc): UserSchema => { + delete row.__v; + const { _id: id, ...attributes } = row; + return { + id, + ...attributes + }; +}; + +export const transformSessionDoc = (row: SessionDoc): SessionSchema => { + delete row.__v; + const { _id: id, ...attributes } = row; + return { + id, + ...attributes + }; +}; + +export const transformKeyDoc = (row: KeyDoc): KeySchema => { + return { + id: row._id, + user_id: row.user_id, + hashed_password: row.hashed_password ?? null + }; +}; diff --git a/packages/adapter-mongoose/src/utils.ts b/packages/adapter-mongoose/src/utils.ts deleted file mode 100644 index 6d4e1feaa..000000000 --- a/packages/adapter-mongoose/src/utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { KeySchema, SessionSchema, UserSchema } from "lucia-auth"; -import type { UserDoc, SessionDoc, KeyDoc } from "./docs.js"; - -export const transformUserDoc = (row: UserDoc): UserSchema => { - delete row.$__; - delete row.__v; - delete row._doc; - const { _id: id, ...attributes } = row; - return { - id, - ...attributes - }; -}; - -export const transformSessionDoc = (row: SessionDoc): SessionSchema => { - return { - id: row._id, - user_id: row.user_id, - active_expires: row.active_expires, - idle_expires: row.idle_expires - }; -}; - -export const transformKeyDoc = (row: KeyDoc): KeySchema => { - return { - id: row._id, - user_id: row.user_id, - hashed_password: row.hashed_password ?? null, - primary_key: row.primary_key, - expires: row.expires ?? null - }; -}; diff --git a/packages/adapter-mongoose/test/db.ts b/packages/adapter-mongoose/test/db.ts index afd81e2b1..1d0b87657 100644 --- a/packages/adapter-mongoose/test/db.ts +++ b/packages/adapter-mongoose/test/db.ts @@ -1,46 +1,36 @@ -import mongoose from "mongoose"; -import type { - LuciaQueryHandler, - TestUserSchema -} from "@lucia-auth/adapter-test"; -import mongodb from "../src/index.js"; - +import mongodb from "mongoose"; import dotenv from "dotenv"; import { resolve } from "path"; -import { transformKeyDoc, transformSessionDoc } from "../src/utils.js"; -import { LuciaError } from "lucia-auth"; dotenv.config({ path: `${resolve()}/.env` }); -const url = process.env.MONGODB_URL; - -if (!url) throw new Error(".env is not set up"); - -const User = mongoose.model( - "auth_user", - new mongoose.Schema( +export const User = mongodb.model( + "User", + new mongodb.Schema( { _id: { - type: String + type: String, + required: true }, username: { unique: true, type: String, required: true } - }, + } as const, { _id: false } ) ); -const Session = mongoose.model( - "auth_session", - new mongoose.Schema( +export const Session = mongodb.model( + "Session", + new mongodb.Schema( { _id: { - type: String + type: String, + required: true }, user_id: { type: String, @@ -53,99 +43,34 @@ const Session = mongoose.model( idle_expires: { type: Number, required: true + }, + country: { + type: String, + required: true } - }, + } as const, { _id: false } ) ); -const Key = mongoose.model( - "auth_key", - new mongoose.Schema( +export const Key = mongodb.model( + "Key", + new mongodb.Schema( { _id: { - type: String - }, - user_id: { type: String, required: true }, - hashed_password: String, - primary_key: { - type: Boolean, + user_id: { + type: String, required: true }, - expires: Number - }, + hashed_password: String + } as const, { _id: false } ) ); -const clientPromise = mongoose.connect(url); - -export const adapter = mongodb(mongoose)(LuciaError); - -const inputToMongooseDoc = (obj: Record) => { - if (obj.id === undefined) return obj; - const { id, ...data } = obj; - return { - _id: id, - ...data - }; -}; -export const queryHandler: LuciaQueryHandler = { - user: { - get: async () => { - await clientPromise; - const userDocs = await User.find().lean(); - return userDocs.map((doc) => { - const { _id: id, ...attributes } = doc; - return { - id, - ...attributes - } as TestUserSchema; - }); - }, - insert: async (user) => { - await clientPromise; - const userDoc = new User(inputToMongooseDoc(user)); - await userDoc.save(); - }, - clear: async () => { - await clientPromise; - await User.deleteMany().lean(); - } - }, - session: { - get: async () => { - await clientPromise; - const sessionDocs = await Session.find().lean(); - return sessionDocs.map((doc) => transformSessionDoc(doc)); - }, - insert: async (session) => { - await clientPromise; - const sessionDoc = new Session(inputToMongooseDoc(session)); - await sessionDoc.save(); - }, - clear: async () => { - await clientPromise; - await Session.deleteMany().lean(); - } - }, - key: { - get: async () => { - await clientPromise; - const keyDocs = await Key.find().lean(); - return keyDocs.map((doc) => transformKeyDoc(doc)); - }, - insert: async (key) => { - await clientPromise; - const keyDoc = new Key(inputToMongooseDoc(key)); - await keyDoc.save(); - }, - clear: async () => { - await clientPromise; - await Key.deleteMany().lean(); - } - } +export const connect = async () => { + await mongodb.connect(process.env.MONGODB_URL as any); }; diff --git a/packages/adapter-mongoose/test/index.ts b/packages/adapter-mongoose/test/index.ts index b90f131ca..e2125469c 100644 --- a/packages/adapter-mongoose/test/index.ts +++ b/packages/adapter-mongoose/test/index.ts @@ -1,4 +1,63 @@ -import { testAdapter } from "@lucia-auth/adapter-test"; -import { queryHandler, adapter } from "./db.js"; +import { Model } from "mongoose"; +import { testAdapter, Database } from "@lucia-auth/adapter-test"; +import { LuciaError } from "lucia"; -testAdapter(adapter, queryHandler); +import { mongoose } from "../src/index.js"; +import { + createMongoValues, + transformKeyDoc, + transformSessionDoc, + transformUserDoc +} from "../src/mongoose.js"; +import { User, Key, Session, connect } from "./db.js"; + +import type { QueryHandler, TableQueryHandler } from "@lucia-auth/adapter-test"; + +const createPartialTableQueryHandler = ( + Model: Model +): Pick => { + return { + insert: async (value) => { + const sessionDoc = new Model(createMongoValues(value)); + await sessionDoc.save(); + }, + clear: async () => { + await Model.deleteMany(); + } + }; +}; + +const queryHandler: QueryHandler = { + user: { + get: async () => { + const userDocs = await User.find().lean(); + return userDocs.map((doc) => transformUserDoc(doc) as any); + }, + ...createPartialTableQueryHandler(User) + }, + session: { + get: async () => { + const sessionDocs = await Session.find().lean(); + return sessionDocs.map((doc) => transformSessionDoc(doc)); + }, + ...createPartialTableQueryHandler(Session) + }, + key: { + get: async () => { + const keyDocs = await Key.find().lean(); + return keyDocs.map((doc) => transformKeyDoc(doc)); + }, + ...createPartialTableQueryHandler(Key) + } +}; + +const adapter = mongoose({ + User, + Session, + Key +})(LuciaError); + +await connect(); +await testAdapter(adapter, new Database(queryHandler)); + +process.exit(0); diff --git a/packages/adapter-mysql/.env.example b/packages/adapter-mysql/.env.example new file mode 100644 index 000000000..f79d76db0 --- /dev/null +++ b/packages/adapter-mysql/.env.example @@ -0,0 +1,6 @@ +MYSQL2_DATABASE="" +MYSQL2_PASSWORD="" + +PLANETSCALE_HOST="" +PLANETSCALE_USERNAME="" +PLANETSCALE_PASSWORD="" \ No newline at end of file diff --git a/packages/adapter-mysql/.gitignore b/packages/adapter-mysql/.gitignore index 27a3b5d40..7a0ae98eb 100644 --- a/packages/adapter-mysql/.gitignore +++ b/packages/adapter-mysql/.gitignore @@ -1,7 +1,5 @@ /node_modules /dist .DS_Store -/prisma/migrations -/prisma/dev.db -/prisma/dev.db-journal .env +*.tgz \ No newline at end of file diff --git a/packages/adapter-mysql/.npmignore b/packages/adapter-mysql/.npmignore deleted file mode 100644 index e1ac86668..000000000 --- a/packages/adapter-mysql/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -/node_modules -.DS_Store -/src -/tsconfig.json -.gitignore -.env -/prisma -/test \ No newline at end of file diff --git a/packages/adapter-mysql/README.md b/packages/adapter-mysql/README.md index 61b35b6c5..3a58770f2 100644 --- a/packages/adapter-mysql/README.md +++ b/packages/adapter-mysql/README.md @@ -48,7 +48,7 @@ Set up env var: ```bash PLANETSCALE_HOST="" PLANETSCALE_USERNAME="" -PLANETSCALE_PASSWORD= "" +PLANETSCALE_PASSWORD="" ``` Run: diff --git a/packages/adapter-mysql/package.json b/packages/adapter-mysql/package.json index 83026f237..98f304132 100644 --- a/packages/adapter-mysql/package.json +++ b/packages/adapter-mysql/package.json @@ -2,43 +2,44 @@ "name": "@lucia-auth/adapter-mysql", "version": "1.1.1", "description": "MySQL adapter for Lucia", - "main": "index.js", - "types": "index.d.ts", - "module": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "module": "dist/index.js", "type": "module", "files": [ - "**/*" + "/dist/", + "CHANGELOG.md" ], "scripts": { - "build": "shx rm -rf ./dist/* && tsc && shx cp ./package.json ./dist && shx cp ./README.md ./dist && shx cp .npmignore dist", - "test.mysql2": " tsx test/mysql2/index.ts", - "test.planetscale": " tsx test/planetscale/index.ts", - "auri.publish": "pnpm build && cd dist && pnpm install --no-frozen-lockfile && pnpm publish --no-git-checks --access public && cd ../" + "build": "shx rm -rf ./dist/* && tsc", + "test.mysql2": "tsx test/mysql2/index.ts", + "test.planetscale": "tsx test/planetscale/index.ts", + "test-setup.planetscale": "tsx test/planetscale/setup.ts", + "test-setup.mysql2": "tsx test/mysql2/setup.ts", + "auri.publish": "pnpm build && pnpm publish --no-git-checks --access public" }, "keywords": [ "lucia", - "lucia-auth", "auth", + "mysql2", "mysql", - "mysql", + "planetscale", "authentication", "adapter", - "sql", - "kysely", - "drizzle" + "sql" ], "repository": { "type": "git", "url": "https://github.com/pilcrowOnPaper/lucia", - "directory": "packages/adapter-prisma" + "directory": "packages/adapter-mysql" }, "author": "pilcrowonpaper", "license": "MIT", "exports": { - ".": "./index.js" + ".": "./dist/index.js" }, "peerDependencies": { - "lucia-auth": "^1.4.0", + "lucia": "^2.0.0", "mysql2": "^3.0.0", "@planetscale/database": "^1.0.0" }, @@ -51,10 +52,10 @@ } }, "devDependencies": { - "@lucia-auth/adapter-test": "workspace:*", + "@lucia-auth/adapter-test": "latest", "@planetscale/database": "^1.7.0", "dotenv": "^16.0.3", - "lucia-auth": "workspace:*", + "lucia": "latest", "mysql2": "^3.2.3", "tsx": "^3.12.6" } diff --git a/packages/adapter-mysql/src/core.ts b/packages/adapter-mysql/src/core.ts deleted file mode 100644 index c54b776a9..000000000 --- a/packages/adapter-mysql/src/core.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { transformDatabaseSession, transformDatabaseKey } from "./utils.js"; - -import type { - MySQLUserSchema, - MySQLSessionSchema, - MySQLKeySchema -} from "./utils.js"; -import type { Adapter, KeySchema, SessionSchema } from "lucia-auth"; -import type { Operator } from "./query.js"; - -export const createCoreAdapter = (operator: Operator) => { - return { - getUser: async (userId) => { - return operator.get((ctx) => [ - ctx.selectFrom("auth_user", "*"), - ctx.where("id", "=", userId) - ]); - }, - getSessionAndUserBySessionId: async (sessionId) => { - const data = await operator.get< - MySQLUserSchema & { - _session_active_expires: number; - _session_id: string; - _session_idle_expires: number; - _session_user_id: string; - } - >((ctx) => [ - ctx.selectFrom( - "auth_session", - "auth_user.*", - "auth_session.id as _session_id", - "auth_session.active_expires as _session_active_expires", - "auth_session.idle_expires as _session_idle_expires", - "auth_session.user_id as _session_user_id" - ), - ctx.innerJoin("auth_user", "auth_user.id", "auth_session.user_id"), - ctx.where("auth_session.id", "=", sessionId) - ]); - if (!data) return null; - const { - _session_active_expires, - _session_id, - _session_idle_expires, - _session_user_id, - ...user - } = data; - return { - user, - session: transformDatabaseSession({ - id: _session_id, - user_id: _session_user_id, - active_expires: _session_active_expires, - idle_expires: _session_idle_expires - }) - }; - }, - getSession: async (sessionId) => { - const databaseSession = await operator.get((ctx) => [ - ctx.selectFrom("auth_session", "*"), - ctx.where("id", "=", sessionId) - ]); - if (!databaseSession) return null; - return transformDatabaseSession(databaseSession); - }, - getSessionsByUserId: async (userId) => { - const databaseSessions = await operator.getAll( - (ctx) => [ - ctx.selectFrom("auth_session", "*"), - ctx.where("user_id", "=", userId) - ] - ); - return databaseSessions.map((val) => transformDatabaseSession(val)); - }, - deleteUser: async (userId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_user"), - ctx.where("id", "=", userId) - ]); - }, - setSession: async (session) => { - await operator.run((ctx) => [ctx.insertInto("auth_session", session)]); - }, - deleteSession: async (sessionId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_session"), - ctx.where("id", "=", sessionId) - ]); - }, - deleteSessionsByUserId: async (userId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_session"), - ctx.where("user_id", "=", userId) - ]); - }, - updateUserAttributes: async (userId, attributes) => { - if (Object.keys(attributes).length === 0) { - await operator.run((ctx) => [ - ctx.selectFrom("auth_user", "*"), - ctx.where("id", "=", userId) - ]); - return; - } - await operator.run((ctx) => [ - ctx.update("auth_user", attributes), - ctx.where("id", "=", userId) - ]); - }, - setKey: async (key) => { - await operator.run((ctx) => [ctx.insertInto("auth_key", key)]); - }, - getKey: async (keyId) => { - const databaseKey = await operator.get((ctx) => [ - ctx.selectFrom("auth_key", "*"), - ctx.where("id", "=", keyId) - ]); - if (!databaseKey) return null; - const transformedDatabaseKey = transformDatabaseKey(databaseKey); - return transformedDatabaseKey; - }, - getKeysByUserId: async (userId) => { - const databaseKeys = await operator.getAll((ctx) => [ - ctx.selectFrom("auth_key", "*"), - ctx.where("user_id", "=", userId) - ]); - return databaseKeys.map((val) => transformDatabaseKey(val)); - }, - updateKeyPassword: async (key, hashedPassword) => { - await operator.run((ctx) => [ - ctx.update("auth_key", { - hashed_password: hashedPassword - }), - ctx.where("id", "=", key) - ]); - }, - deleteKeysByUserId: async (userId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_key"), - ctx.where("user_id", "=", userId) - ]); - }, - deleteNonPrimaryKey: async (keyId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_key"), - ctx.and( - ctx.where("id", "=", keyId), - ctx.where("primary_key", "=", false) - ) - ]); - } - } satisfies Partial; -}; - -export const createQueryHelper = (operator: Operator) => { - return { - getUser: async (userId: string) => { - return await operator.get((ctx) => [ - ctx.selectFrom("auth_user", "*"), - ctx.where("id", "=", userId) - ]); - }, - insertUser: async (userId: string, attributes: Record) => { - const user = { - id: userId, - ...attributes - }; - return await operator.run((ctx) => [ - ctx.insertInto("auth_user", user) - ]); - }, - insertSession: async (session: SessionSchema) => { - await operator.run((ctx) => [ctx.insertInto("auth_session", session)]); - }, - insertKey: async (key: KeySchema) => { - await operator.run((ctx) => [ctx.insertInto("auth_key", key)]); - } - }; -}; diff --git a/packages/adapter-mysql/src/drivers/mysql2.ts b/packages/adapter-mysql/src/drivers/mysql2.ts new file mode 100644 index 000000000..3bef5fe93 --- /dev/null +++ b/packages/adapter-mysql/src/drivers/mysql2.ts @@ -0,0 +1,298 @@ +import { helper, getSetArgs, escapeName } from "../utils.js"; + +import type { + SessionSchema, + Adapter, + InitializeAdapter, + UserSchema, + KeySchema +} from "lucia"; +import type { + Pool, + QueryError, + RowDataPacket, + OkPacket, + ResultSetHeader, + PoolConnection +} from "mysql2/promise"; + +export const mysql2Adapter = ( + db: Pool, + tables: { + user: string; + session: string; + key: string; + } +): InitializeAdapter => { + const ESCAPED_USER_TABLE_NAME = escapeName(tables.user); + const ESCAPED_SESSION_TABLE_NAME = escapeName(tables.session); + const ESCAPED_KEY_TABLE_NAME = escapeName(tables.key); + + const transaction = async < + _Execute extends (connection: PoolConnection) => Promise + >( + execute: _Execute + ) => { + const connection = await db.getConnection(); + try { + await connection.beginTransaction(); + await execute(connection); + await connection.commit(); + return; + } catch (e) { + await connection.rollback(); + throw e; + } + }; + return (LuciaError) => { + return { + getUser: async (userId) => { + const result = await get( + db.query(`SELECT * FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = ?`, [ + userId + ]) + ); + return result; + }, + setUser: async (user, key) => { + if (!key) { + const [userFields, userValues, userArgs] = helper(user); + await db.execute( + `INSERT INTO ${ESCAPED_USER_TABLE_NAME} ( ${userFields} ) VALUES ( ${userValues} )`, + userArgs + ); + return; + } + try { + await transaction(async (connection) => { + const [userFields, userValues, userArgs] = helper(user); + await connection.execute( + `INSERT INTO ${ESCAPED_USER_TABLE_NAME} ( ${userFields} ) VALUES ( ${userValues} )`, + userArgs + ); + const [keyFields, keyValues, keyArgs] = helper(key); + await connection.execute( + `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${keyFields} ) VALUES ( ${keyValues} )`, + keyArgs + ); + }); + } catch (e) { + const error = e as Partial; + if ( + error.code === "ER_DUP_ENTRY" && + error.message?.includes("PRIMARY") && + error.message?.includes(tables.key) + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw e; + } + }, + deleteUser: async (userId) => { + await db.query(`DELETE FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = ?`, [ + userId + ]); + }, + updateUser: async (userId, partialUser) => { + const [fields, values, args] = helper(partialUser); + await db.execute( + `UPDATE ${ESCAPED_USER_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?`, + [...args, userId] + ); + }, + + getSession: async (sessionId) => { + const result = await get( + db.query(`SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`, [ + sessionId + ]) + ); + return result; + }, + getSessionsByUserId: async (userId) => { + const result = await getAll( + db.query( + `SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = ?`, + [userId] + ) + ); + return result; + }, + setSession: async (session) => { + try { + const [fields, values, args] = helper(session); + await db.execute( + `INSERT INTO ${ESCAPED_SESSION_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )`, + args + ); + } catch (e) { + const error = e as Partial; + if (error.errno === 1452 && error.message?.includes("(`user_id`)")) { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + throw e; + } + }, + deleteSession: async (sessionId) => { + await db.execute( + `DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`, + [sessionId] + ); + }, + deleteSessionsByUserId: async (userId) => { + await db.execute( + `DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = ?`, + [userId] + ); + }, + updateSession: async (sessionId, partialSession) => { + const [fields, values, args] = helper(partialSession); + await db.execute( + `UPDATE ${ESCAPED_SESSION_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?`, + [...args, sessionId] + ); + }, + + getKey: async (keyId) => { + const result = await get( + db.query(`SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = ?`, [ + keyId + ]) + ); + return result; + }, + getKeysByUserId: async (userId) => { + const result = getAll( + db.execute( + `SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = ?`, + [userId] + ) + ); + return result; + }, + setKey: async (key) => { + try { + const [fields, values, args] = helper(key); + await db.execute( + `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )`, + args + ); + } catch (e) { + const error = e as Partial; + if (error.errno === 1452 && error.message?.includes("(`user_id`)")) { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + if ( + error.code === "ER_DUP_ENTRY" && + error.message?.includes("PRIMARY") && + error.message?.includes(tables.key) + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw e; + } + }, + deleteKey: async (keyId) => { + await db.execute(`DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = ?`, [ + keyId + ]); + }, + deleteKeysByUserId: async (userId) => { + await db.execute( + `DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = ?`, + [userId] + ); + }, + updateKey: async (keyId, partialKey) => { + const [fields, values, args] = helper(partialKey); + await db.execute( + `UPDATE ${ESCAPED_KEY_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?`, + [...args, keyId] + ); + }, + + getSessionAndUser: async (sessionId) => { + const getSessionPromise = get( + db.query(`SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`, [ + sessionId + ]) + ); + const getUserFromJoinPromise = get< + UserSchema & { + __session_id: string; + } + >( + db.query( + `SELECT ${ESCAPED_USER_TABLE_NAME}.*, ${ESCAPED_SESSION_TABLE_NAME}.id as __session_id FROM ${ESCAPED_SESSION_TABLE_NAME} INNER JOIN ${ESCAPED_USER_TABLE_NAME} ON ${ESCAPED_USER_TABLE_NAME}.id = ${ESCAPED_SESSION_TABLE_NAME}.user_id WHERE ${ESCAPED_SESSION_TABLE_NAME}.id = ?`, + [sessionId] + ) + ); + const [sessionResult, userFromJoinResult] = await Promise.all([ + getSessionPromise, + getUserFromJoinPromise + ]); + if (!sessionResult || !userFromJoinResult) return [null, null]; + const { __session_id: _, ...userResult } = userFromJoinResult; + return [sessionResult, userResult]; + } + }; + }; +}; + +const isPacketArray = ( + maybeRowDataPacketArray: RowDataPacket[] | RowDataPacket[][] | OkPacket[] +): maybeRowDataPacketArray is RowDataPacket[] => { + const firstVal = maybeRowDataPacketArray.at(0) ?? null; + if (!firstVal) return true; + if (!Array.isArray(firstVal)) return true; + return false; +}; +export const get = async ( + queryPromise: Promise< + [ + ( + | RowDataPacket[] + | RowDataPacket[][] + | OkPacket + | OkPacket[] + | ResultSetHeader + ), + any + ] + > +): Promise => { + const [rows] = await queryPromise; + if (!Array.isArray(rows)) return null; + const result = rows.at(0) ?? null; + if (!result || Array.isArray(result)) return null; + return result as any; +}; + +export const getAll = async ( + queryPromise: Promise< + [ + ( + | RowDataPacket[] + | RowDataPacket[][] + | OkPacket + | OkPacket[] + | ResultSetHeader + ), + any + ] + > +): Promise => { + const [rows] = await queryPromise; + if (!Array.isArray(rows)) return []; + if (!isPacketArray(rows)) return []; + return rows as any; +}; diff --git a/packages/adapter-mysql/src/drivers/planetscale.ts b/packages/adapter-mysql/src/drivers/planetscale.ts new file mode 100644 index 000000000..9965ec260 --- /dev/null +++ b/packages/adapter-mysql/src/drivers/planetscale.ts @@ -0,0 +1,256 @@ +import { escapeName, getSetArgs, helper } from "../utils.js"; + +import type { + Connection, + DatabaseError, + ExecutedQuery +} from "@planetscale/database"; +import type { + Adapter, + InitializeAdapter, + UserSchema, + SessionSchema, + KeySchema +} from "lucia"; + +export const planetscaleAdapter = ( + connection: Connection, + tables: { + user: string; + session: string; + key: string; + } +): InitializeAdapter => { + const ESCAPED_USER_TABLE_NAME = escapeName(tables.user); + const ESCAPED_SESSION_TABLE_NAME = escapeName(tables.session); + const ESCAPED_KEY_TABLE_NAME = escapeName(tables.key); + + return (LuciaError) => { + return { + getUser: async (userId) => { + const result = await get( + connection.execute( + `SELECT * FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = ?`, + [userId] + ) + ); + return result; + }, + setUser: async (user, key) => { + if (!key) { + const [userFields, userValues, userArgs] = helper(user); + await connection.execute( + `INSERT INTO ${ESCAPED_USER_TABLE_NAME} ( ${userFields} ) VALUES ( ${userValues} )`, + userArgs + ); + return; + } + try { + await connection.transaction(async (tx) => { + const [userFields, userValues, userArgs] = helper(user); + await tx.execute( + `INSERT INTO ${ESCAPED_USER_TABLE_NAME} ( ${userFields} ) VALUES ( ${userValues} )`, + userArgs + ); + const [keyFields, keyValues, keyArgs] = helper(key); + await tx.execute( + `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${keyFields} ) VALUES ( ${keyValues} )`, + keyArgs + ); + }); + } catch (e) { + const error = e as Partial; + if ( + error.body?.message.includes("AlreadyExists") && + error.body?.message.includes("PRIMARY") && + error.body?.message.includes(`${tables.key}`) + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw e; + } + }, + deleteUser: async (userId) => { + await connection.execute( + `DELETE FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = ?`, + [userId] + ); + }, + updateUser: async (userId, partialUser) => { + const [fields, values, args] = helper(partialUser); + await connection.execute( + `UPDATE ${ESCAPED_USER_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?`, + [...args, userId] + ); + }, + + getSession: async (sessionId) => { + const result = await get( + connection.execute( + `SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`, + [sessionId] + ) + ); + return result ? transformPlanetscaleSession(result) : null; + }, + getSessionsByUserId: async (userId) => { + const result = await getAll( + connection.execute( + `SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = ?`, + [userId] + ) + ); + return result.map((val) => transformPlanetscaleSession(val)); + }, + setSession: async (session) => { + const [fields, values, args] = helper(session); + await connection.execute( + `INSERT INTO ${ESCAPED_SESSION_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )`, + args + ); + }, + deleteSession: async (sessionId) => { + await connection.execute( + `DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`, + [sessionId] + ); + }, + deleteSessionsByUserId: async (userId) => { + await connection.execute( + `DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = ?`, + [userId] + ); + }, + updateSession: async (sessionId, partialSession) => { + const [fields, values, args] = helper(partialSession); + await connection.execute( + `UPDATE ${ESCAPED_SESSION_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?`, + [...args, sessionId] + ); + }, + + getKey: async (keyId) => { + const result = await get( + connection.execute( + `SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = ?`, + [keyId] + ) + ); + return result; + }, + getKeysByUserId: async (userId) => { + const result = getAll( + connection.execute( + `SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = ?`, + [userId] + ) + ); + return result; + }, + setKey: async (key) => { + try { + const [fields, values, args] = helper(key); + await connection.execute( + `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )`, + args + ); + } catch (e) { + const error = e as Partial; + if ( + error.body?.message.includes("AlreadyExists") && + error.body?.message.includes("PRIMARY") && + error.body?.message.includes(`${tables.key}`) + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw e; + } + }, + deleteKey: async (keyId) => { + await connection.execute( + `DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = ?`, + [keyId] + ); + }, + deleteKeysByUserId: async (userId) => { + await connection.execute( + `DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = ?`, + [userId] + ); + }, + updateKey: async (keyId, partialKey) => { + const [fields, values, args] = helper(partialKey); + await connection.execute( + `UPDATE ${ESCAPED_KEY_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?`, + [...args, keyId] + ); + }, + + getSessionAndUser: async (sessionId) => { + const [sessionResult, userFromJoinResult] = await Promise.all([ + get( + connection.execute( + `SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`, + [sessionId] + ) + ), + get< + UserSchema & { + __session_id: string; + } + >( + connection.execute( + `SELECT ${ESCAPED_USER_TABLE_NAME}.*, ${ESCAPED_SESSION_TABLE_NAME}.id as __session_id FROM ${ESCAPED_SESSION_TABLE_NAME} INNER JOIN ${ESCAPED_USER_TABLE_NAME} ON ${ESCAPED_USER_TABLE_NAME}.id = ${ESCAPED_SESSION_TABLE_NAME}.user_id WHERE ${ESCAPED_SESSION_TABLE_NAME}.id = ?`, + [sessionId] + ) + ) + ]); + if (!sessionResult || !userFromJoinResult) return [null, null]; + const { __session_id: _, ...userResult } = userFromJoinResult; + return [transformPlanetscaleSession(sessionResult), userResult]; + } + }; + }; +}; + +export const get = async ( + queryPromise: Promise +): Promise => { + const { rows } = await queryPromise; + const result = rows.at(0) ?? null; + return result as any; +}; + +export const getAll = async ( + queryPromise: Promise +): Promise => { + const { rows } = await queryPromise; + return rows as any; +}; + +export type PlanetscaleSession = Omit< + SessionSchema, + "active_expires" | "idle_expires" +> & { + active_expires: BigInt; + idle_expires: BigInt; +}; + +export const transformPlanetscaleSession = ( + session: PlanetscaleSession +): SessionSchema => { + return { + ...session, + active_expires: Number(session.active_expires), + idle_expires: Number(session.idle_expires) + }; +}; diff --git a/packages/adapter-mysql/src/index.ts b/packages/adapter-mysql/src/index.ts index 297f5d791..dda5e9380 100644 --- a/packages/adapter-mysql/src/index.ts +++ b/packages/adapter-mysql/src/index.ts @@ -1,2 +1,2 @@ -export { mysql2Adapter as mysql2 } from "./mysql2/index.js"; -export { planetscaleAdapter as planetscale } from "./planetscale/index.js"; +export { mysql2Adapter as mysql2 } from "./drivers/mysql2.js"; +// export { planetscaleAdapter as planetscale } from "./planetscale/index.js"; diff --git a/packages/adapter-mysql/src/lucia.d.ts b/packages/adapter-mysql/src/lucia.d.ts index f61de1891..8026ca988 100644 --- a/packages/adapter-mysql/src/lucia.d.ts +++ b/packages/adapter-mysql/src/lucia.d.ts @@ -1,5 +1,6 @@ -/// +/// declare namespace Lucia { type Auth = any; - type UserAttributes = {}; + type DatabaseUserAttributes = any; + type DatabaseSessionAttributes = any; } diff --git a/packages/adapter-mysql/src/mysql2/index.ts b/packages/adapter-mysql/src/mysql2/index.ts deleted file mode 100644 index d0196f9be..000000000 --- a/packages/adapter-mysql/src/mysql2/index.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { mysql2Runner } from "./runner.js"; -import { createCoreAdapter, createQueryHelper } from "../core.js"; -import { createOperator } from "../query.js"; - -import type { Adapter, AdapterFunction } from "lucia-auth"; -import type { Pool, QueryError } from "mysql2/promise"; - -export const mysql2Adapter = (db: Pool): AdapterFunction => { - const transaction = async <_Execute extends () => Promise>( - execute: _Execute - ) => { - const connection = await db.getConnection(); - try { - await connection.beginTransaction(); - await execute(); - await connection.commit(); - return; - } catch (e) { - await connection.rollback(); - throw e; - } - }; - - return (LuciaError) => { - const operator = createOperator(mysql2Runner(db)); - const coreAdapter = createCoreAdapter(operator); - const helper = createQueryHelper(operator); - return { - ...coreAdapter, - setUser: async (userId, attributes, key) => { - try { - if (key) { - await transaction(async () => { - await helper.insertUser(userId, attributes); - await helper.insertKey(key); - }); - return; - } - await helper.insertUser(userId, attributes); - } catch (e) { - const error = e as Partial; - if ( - error.code === "ER_DUP_ENTRY" && - error.message?.includes("PRIMARY") - ) { - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - throw e; - } - }, - setSession: async (session) => { - try { - const user = await helper.getUser(session.user_id); - if (!user) throw new LuciaError("AUTH_INVALID_USER_ID"); - await helper.insertSession(session); - } catch (e) { - const error = e as Partial; - if (error.errno === 1452 && error.message?.includes("(`user_id`)")) { - throw new LuciaError("AUTH_INVALID_USER_ID"); - } - if ( - error.code === "ER_DUP_ENTRY" && - error.message?.includes("PRIMARY") - ) { - throw new LuciaError("AUTH_DUPLICATE_SESSION_ID"); - } - throw e; - } - }, - setKey: async (key) => { - try { - const user = await helper.getUser(key.user_id); - if (!user) throw new LuciaError("AUTH_INVALID_USER_ID"); - await helper.insertKey(key); - } catch (e) { - const error = e as Partial; - if (error.errno === 1452 && error.message?.includes("(`user_id`)")) { - throw new LuciaError("AUTH_INVALID_USER_ID"); - } - if ( - error.code === "ER_DUP_ENTRY" && - error.message?.includes("PRIMARY") - ) { - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - throw e; - } - } - }; - }; -}; diff --git a/packages/adapter-mysql/src/mysql2/runner.ts b/packages/adapter-mysql/src/mysql2/runner.ts deleted file mode 100644 index e1b7bf06e..000000000 --- a/packages/adapter-mysql/src/mysql2/runner.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Pool } from "mysql2/promise"; -import type { Runner } from "../query.js"; - -export const mysql2Runner = (pool: Pool): Runner => { - return { - get: async (query, params) => { - const [rows] = await pool.query(query, params); - return rows; - }, - run: async (query, params) => { - await pool.query(query, params); - } - }; -}; diff --git a/packages/adapter-mysql/src/planetscale/index.ts b/packages/adapter-mysql/src/planetscale/index.ts deleted file mode 100644 index fc854134a..000000000 --- a/packages/adapter-mysql/src/planetscale/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { createCoreAdapter, createQueryHelper } from "../core.js"; -import { planetscaleRunner } from "./runner.js"; -import { createOperator } from "../query.js"; - -import type { Connection, DatabaseError } from "@planetscale/database"; -import type { Adapter, AdapterFunction } from "lucia-auth"; - -export const planetscaleAdapter = ( - connection: Connection -): AdapterFunction => { - return (LuciaError) => { - const operator = createOperator(planetscaleRunner(connection)); - const coreAdapter = createCoreAdapter(operator); - const helper = createQueryHelper(operator); - return { - ...coreAdapter, - setUser: async (userId, attributes, key) => { - try { - if (key) { - await connection.transaction(async (trx) => { - const trxOperator = createOperator(planetscaleRunner(trx)); - const trxHelper = createQueryHelper(trxOperator); - await trxHelper.insertUser(userId, attributes); - await trxHelper.insertKey(key); - }); - return; - } - await helper.insertUser(userId, attributes); - return; - } catch (e) { - const error = e as Partial; - if ( - error.body?.message.includes("AlreadyExists") && - error.body?.message.includes("PRIMARY") - ) { - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - throw e; - } - }, - setSession: async (session) => { - try { - const user = await helper.getUser(session.user_id); - if (!user) throw new LuciaError("AUTH_INVALID_USER_ID"); - await helper.insertSession(session); - } catch (e) { - const error = e as Partial; - if ( - error.body?.message.includes("AlreadyExists") && - error.body?.message.includes("PRIMARY") - ) { - throw new LuciaError("AUTH_DUPLICATE_SESSION_ID"); - } - throw e; - } - }, - setKey: async (key) => { - try { - const user = await helper.getUser(key.user_id); - if (!user) throw new LuciaError("AUTH_INVALID_USER_ID"); - await helper.insertKey(key); - } catch (e) { - const error = e as Partial; - if ( - error.body?.message.includes("AlreadyExists") && - error.body?.message.includes("PRIMARY") - ) { - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - throw e; - } - } - }; - }; -}; diff --git a/packages/adapter-mysql/src/planetscale/runner.ts b/packages/adapter-mysql/src/planetscale/runner.ts deleted file mode 100644 index ba773d1ea..000000000 --- a/packages/adapter-mysql/src/planetscale/runner.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Connection } from "@planetscale/database"; -import type { Runner } from "../query.js"; - -export const planetscaleRunner = ( - connection: Pick -): Runner => { - return { - get: async (query, params) => { - const { rows } = await connection.execute(query, params); - return rows; - }, - run: async (query, params) => { - await connection.execute(query, params); - } - }; -}; diff --git a/packages/adapter-mysql/src/query.ts b/packages/adapter-mysql/src/query.ts deleted file mode 100644 index 91e523023..000000000 --- a/packages/adapter-mysql/src/query.ts +++ /dev/null @@ -1,243 +0,0 @@ -const resolveQueryBlock = (block: Block): ResolvedBlock => { - const escapeName = (val: string) => { - if (val === "*") return val; - return `\`${val}\``; - }; - if (block.type === "DELETE_FROM") { - return { - queryChunk: `DELETE FROM ${escapeName(block.table)}`, - params: [] - }; - } - if (block.type === "INNER_JOIN") { - return { - queryChunk: `INNER JOIN ${escapeName(block.targetTable)} ON ${ - block.targetColumn - } = ${block.column}`, - params: [] - }; - } - if (block.type === "INSERT_INTO") { - const keys = Object.keys(block.values); - return { - queryChunk: `INSERT INTO ${escapeName(block.table)} (${keys.map((k) => - escapeName(k) - )}) VALUES (${Array(keys.length).fill("?")})`, - params: keys.map((k) => block.values[k]) - }; - } - if (block.type === "SELECT") { - return { - queryChunk: `SELECT ${block.columns} FROM ${escapeName(block.table)}`, - params: [] - }; - } - if (block.type === "WHERE") { - return { - queryChunk: `WHERE ${block.column} ${block.comparator} ?`, - params: [block.value] - }; - } - if (block.type === "UPDATE") { - const keys = Object.keys(block.values); - return { - queryChunk: `UPDATE ${escapeName(block.table)} SET ${keys.map((k) => { - return `${escapeName(k)} = ?`; - })}`, - params: keys.map((k) => block.values[k]) - }; - } - if (block.type === "AND") { - const resolvedConditionQueryBlocks = block.whereBlocks.map((whereBlock) => { - return { - queryChunk: `${whereBlock.column} ${whereBlock.comparator} ?`, - params: [whereBlock.value] - }; - }); - const conditionQueryChunk = resolvedConditionQueryBlocks - .map((resolvedBlock) => resolvedBlock.queryChunk) - .join(" AND "); - return { - queryChunk: `WHERE ${conditionQueryChunk}`, - params: resolvedConditionQueryBlocks.reduce( - (acc, curr) => [...acc, ...curr.params], - [] as ColumnValue[] - ) - }; - } - throw new TypeError(`Invalid block type`); -}; - -const ctx = { - innerJoin: (targetTable: string, targetColumn: string, column: string) => { - return { - type: "INNER_JOIN", - targetTable, - targetColumn, - column - }; - }, - selectFrom: (table: string, ...columns: [string, ...string[]]) => { - return { - type: "SELECT", - table, - columns - }; - }, - insertInto: (table: string, values: Record) => { - return { - type: "INSERT_INTO", - table, - values - }; - }, - where: (column: string, comparator: string, value: ColumnValue) => { - return { - type: "WHERE", - column, - comparator, - value - }; - }, - deleteFrom: (table: string) => { - return { - type: "DELETE_FROM", - table - }; - }, - update: (table: string, values: Record) => { - return { - type: "UPDATE", - table, - values - }; - }, - and: (...whereBlocks: WhereBlock[]) => { - return { - type: "AND", - whereBlocks - }; - } -} satisfies Record Block>; - -export const createOperator = <_Runner extends Runner>(runner: _Runner) => { - const resolveQueryBlocks = (blocks: Block[]) => { - const resolvedBlocks = blocks.map(resolveQueryBlock); - const statement = resolvedBlocks.map((block) => block.queryChunk).join(" "); - const params = resolvedBlocks.reduce((result, block) => { - result.push(...block.params); - return result; - }, [] as ColumnValue[]); - return { - statement, - params - }; - }; - - const write = <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ) => { - const blocks = createQueryBlocks(ctx); - return resolveQueryBlocks(blocks); - }; - const get = async <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ): Promise<_Selection | null> => { - const query = write(createQueryBlocks); - const result = await runner.get(query.statement, query.params); - if (Array.isArray(result)) return result.at(0) ?? null; - return result ?? null; - }; - const getAll = <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ): Promise<_Selection[]> => { - const query = write(createQueryBlocks); - const result = runner.get(query.statement, query.params); - if (result instanceof Promise) { - return new Promise(async (resolve) => { - const awaitedResult = await result; - if (!awaitedResult) return resolve([]); - if (!Array.isArray(awaitedResult)) return resolve([awaitedResult]); - return resolve(awaitedResult); - }) as any; - } - if (!result) return [] as any; - if (!Array.isArray(result)) return [result] as any; - return result as any; - }; - const run = <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ): Promise => { - const query = write(createQueryBlocks); - return runner.run(query.statement, query.params) as any; - }; - return { - write, - get, - getAll, - run - } as const; -}; - -export type Operator = ReturnType; - -export type Context = typeof ctx; - -type CreateQueryBlocks = (context: Context) => Block[]; - -type ResolvedBlock = { - queryChunk: string; - params: ColumnValue[]; -}; - -export type Runner = { - get: (statement: string, params: ColumnValue[]) => Promise; - run: (statement: string, params: ColumnValue[]) => Promise; -}; - -type ColumnValue = string | number | null | bigint | boolean; - -type Block = - | { - type: "INNER_JOIN"; - targetTable: string; - targetColumn: string; - column: string; - } - | { - type: "SELECT"; - table: string; - columns: string[]; - } - | { - type: "INSERT_INTO"; - table: string; - values: Record; - } - | { - type: "WHERE"; - column: string; - comparator: string; - value: ColumnValue; - } - | { - type: "AND"; - whereBlocks: WhereBlock[]; - } - | { - type: "DELETE_FROM"; - table: string; - } - | { - type: "UPDATE"; - table: string; - values: Record; - } - | WhereBlock; - -type WhereBlock = { - type: "WHERE"; - column: string; - comparator: string; - value: ColumnValue; -}; diff --git a/packages/adapter-mysql/src/utils.ts b/packages/adapter-mysql/src/utils.ts index 424c7cb8a..7a1d88d73 100644 --- a/packages/adapter-mysql/src/utils.ts +++ b/packages/adapter-mysql/src/utils.ts @@ -1,34 +1,29 @@ -import type { KeySchema, SessionSchema, UserSchema } from "lucia-auth"; - -export const transformDatabaseSession = ( - session: MySQLSessionSchema -): SessionSchema => { - return { - id: session.id, - user_id: session.user_id, - active_expires: Number(session.active_expires), - idle_expires: Number(session.idle_expires) +const createPreparedStatementHelper = ( + placeholder: (index: number) => string +) => { + const helper = ( + values: Record + ): readonly [fields: string[], placeholders: string[], arguments: any[]] => { + const keys = Object.keys(values); + return [ + keys.map((k) => escapeName(k)), + keys.map((_, i) => placeholder(i)), + keys.map((k) => values[k]) + ] as const; }; + return helper; }; -export const transformDatabaseKey = (key: MySQLKeySchema): KeySchema => { - return { - id: key.id, - user_id: key.user_id, - primary_key: Boolean(key.primary_key), - hashed_password: key.hashed_password, - expires: key.expires === null ? null : Number(key.expires) - }; -}; +const ESCAPE_CHAR = "`"; -export type MySQLUserSchema = UserSchema; -export type MySQLSessionSchema = SessionSchema; -export type MySQLKeySchema = TransformToMySQLSchema; +export const escapeName = (val: string) => { + return `${ESCAPE_CHAR}${val}${ESCAPE_CHAR}`; +}; -export type ReplaceBooleanWithNumber = Extract extends never - ? T - : Exclude | number; +export const helper = createPreparedStatementHelper(() => "?"); -export type TransformToMySQLSchema<_Schema extends {}> = { - [K in keyof _Schema]: ReplaceBooleanWithNumber<_Schema[K]>; +export const getSetArgs = (fields: string[], placeholders: string[]) => { + return fields + .map((field, i) => [field, placeholders[i]].join(" = ")) + .join(","); }; diff --git a/packages/adapter-mysql/test/index.ts b/packages/adapter-mysql/test/index.ts deleted file mode 100644 index 09235c751..000000000 --- a/packages/adapter-mysql/test/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { LuciaQueryHandler } from "@lucia-auth/adapter-test"; -import { Runner, createOperator } from "../src/query.js"; -import { - transformDatabaseKey, - transformDatabaseSession -} from "../src/utils.js"; - -import type { MySQLKeySchema, MySQLSessionSchema } from "../src/utils.js"; -import type { TestUserSchema } from "@lucia-auth/adapter-test"; - -export const createQueryHandler = (runner: Runner) => { - const operator = createOperator(runner); - return { - user: { - get: async () => { - return operator.getAll((ctx) => [ - ctx.selectFrom("auth_user", "*") - ]); - }, - insert: async (user) => { - await operator.run((ctx) => [ctx.insertInto("auth_user", user)]); - }, - clear: async () => { - await operator.run((ctx) => [ctx.deleteFrom("auth_user")]); - } - }, - session: { - get: async () => { - const databaseSessions = await operator.getAll( - (ctx) => [ctx.selectFrom("auth_session", "*")] - ); - return databaseSessions.map((val) => transformDatabaseSession(val)); - }, - insert: async (key) => { - await operator.run((ctx) => [ctx.insertInto("auth_session", key)]); - }, - clear: async () => { - await operator.run((ctx) => [ctx.deleteFrom("auth_session")]); - } - }, - key: { - get: async () => { - const databaseKeys = await operator.getAll((ctx) => [ - ctx.selectFrom("auth_key", "*") - ]); - return databaseKeys.map((val) => transformDatabaseKey(val)); - }, - insert: async (key) => { - await operator.run((ctx) => [ctx.insertInto("auth_key", key)]); - }, - clear: async () => { - await operator.run((ctx) => [ctx.deleteFrom("auth_key")]); - } - } - } satisfies LuciaQueryHandler; -}; diff --git a/packages/adapter-mysql/test/mysql2/db.ts b/packages/adapter-mysql/test/mysql2/db.ts index 961705c71..1ac9963aa 100644 --- a/packages/adapter-mysql/test/mysql2/db.ts +++ b/packages/adapter-mysql/test/mysql2/db.ts @@ -1,20 +1,14 @@ import mysql from "mysql2/promise"; import dotenv from "dotenv"; import { resolve } from "path"; -import { LuciaError } from "lucia-auth"; -import { mysql2 as mysql2Adapter } from "../../src/index.js"; -import { mysql2Runner } from "../../src/mysql2/runner.js"; -import { createQueryHandler } from "../index.js"; dotenv.config({ path: `${resolve()}/.env` }); -const pool = mysql.createPool({ +export const pool = mysql.createPool({ host: "localhost", user: "root", database: process.env.MYSQL2_DATABASE, password: process.env.MYSQL2_PASSWORD }); -export const adapter = mysql2Adapter(pool)(LuciaError); -export const queryHandler = createQueryHandler(mysql2Runner(pool)); diff --git a/packages/adapter-mysql/test/mysql2/index.ts b/packages/adapter-mysql/test/mysql2/index.ts index ee388e4f5..50cbd85aa 100644 --- a/packages/adapter-mysql/test/mysql2/index.ts +++ b/packages/adapter-mysql/test/mysql2/index.ts @@ -1,4 +1,40 @@ -import { testAdapter } from "@lucia-auth/adapter-test"; -import { adapter, queryHandler } from "./db.js"; +import { testAdapter, Database } from "@lucia-auth/adapter-test"; +import { LuciaError } from "lucia"; -testAdapter(adapter, queryHandler); +import { pool } from "./db.js"; +import { escapeName, helper } from "../../src/utils.js"; +import { getAll, mysql2Adapter } from "../../src/drivers/mysql2.js"; +import { TABLE_NAMES } from "../shared.js"; + +import type { QueryHandler, TableQueryHandler } from "@lucia-auth/adapter-test"; + +const createTableQueryHandler = (tableName: string): TableQueryHandler => { + const ESCAPED_TABLE_NAME = escapeName(tableName); + return { + get: async () => { + return await getAll(pool.query(`SELECT * FROM ${ESCAPED_TABLE_NAME}`)); + }, + insert: async (value: any) => { + const [fields, placeholders, args] = helper(value); + await pool.execute( + `INSERT INTO ${ESCAPED_TABLE_NAME} ( ${fields} ) VALUES ( ${placeholders} )`, + args + ); + }, + clear: async () => { + await pool.execute(`DELETE FROM ${ESCAPED_TABLE_NAME}`); + } + }; +}; + +const queryHandler: QueryHandler = { + user: createTableQueryHandler(TABLE_NAMES.user), + session: createTableQueryHandler(TABLE_NAMES.session), + key: createTableQueryHandler(TABLE_NAMES.key) +}; + +const adapter = mysql2Adapter(pool, TABLE_NAMES)(LuciaError); + +await testAdapter(adapter, new Database(queryHandler)); + +process.exit(0); diff --git a/packages/adapter-mysql/test/mysql2/setup.ts b/packages/adapter-mysql/test/mysql2/setup.ts new file mode 100644 index 000000000..3da560c0d --- /dev/null +++ b/packages/adapter-mysql/test/mysql2/setup.ts @@ -0,0 +1,37 @@ +import { pool } from "./db.js"; +import { + ESCAPED_USER_TABLE_NAME, + ESCAPED_SESSION_TABLE_NAME, + ESCAPED_KEY_TABLE_NAME +} from "../shared.js"; + +await pool.execute(` +CREATE TABLE IF NOT EXISTS ${ESCAPED_USER_TABLE_NAME} ( + id VARCHAR(15) PRIMARY KEY, + username VARCHAR(15) NOT NULL UNIQUE +) +`); + +await pool.execute(` +CREATE TABLE IF NOT EXISTS ${ESCAPED_SESSION_TABLE_NAME} ( + id VARCHAR(127) PRIMARY KEY, + user_id VARCHAR(15) NOT NULL, + active_expires BIGINT UNSIGNED NOT NULL, + idle_expires BIGINT UNSIGNED NOT NULL, + country VARCHAR(2) NOT NULL, + + FOREIGN KEY (user_id) REFERENCES ${ESCAPED_USER_TABLE_NAME}(id) +) +`); + +await pool.execute(` +CREATE TABLE IF NOT EXISTS ${ESCAPED_KEY_TABLE_NAME} ( + id VARCHAR(255) PRIMARY KEY, + user_id VARCHAR(15) NOT NULL, + hashed_password VARCHAR(255), + + FOREIGN KEY (user_id) REFERENCES ${ESCAPED_USER_TABLE_NAME}(id) +) +`); + +process.exit(0); diff --git a/packages/adapter-mysql/test/planetscale/db.ts b/packages/adapter-mysql/test/planetscale/db.ts index 6b972899e..a14e38f5c 100644 --- a/packages/adapter-mysql/test/planetscale/db.ts +++ b/packages/adapter-mysql/test/planetscale/db.ts @@ -1,19 +1,13 @@ +import { connect } from "@planetscale/database"; import dotenv from "dotenv"; import { resolve } from "path"; -import { LuciaError } from "lucia-auth"; -import { planetscale as planetscaleAdapter } from "../../src/index.js"; -import { planetscaleRunner } from "../../src/planetscale/runner.js"; -import { createQueryHandler } from "../index.js"; -import { connect } from "@planetscale/database"; dotenv.config({ path: `${resolve()}/.env` }); -const connection = connect({ +export const connection = connect({ host: process.env.PLANETSCALE_HOST, username: process.env.PLANETSCALE_USERNAME, password: process.env.PLANETSCALE_PASSWORD }); -export const adapter = planetscaleAdapter(connection)(LuciaError); -export const queryHandler = createQueryHandler(planetscaleRunner(connection)); diff --git a/packages/adapter-mysql/test/planetscale/index.ts b/packages/adapter-mysql/test/planetscale/index.ts index ee388e4f5..c09b34d57 100644 --- a/packages/adapter-mysql/test/planetscale/index.ts +++ b/packages/adapter-mysql/test/planetscale/index.ts @@ -1,4 +1,55 @@ -import { testAdapter } from "@lucia-auth/adapter-test"; -import { adapter, queryHandler } from "./db.js"; +import { testAdapter, Database } from "@lucia-auth/adapter-test"; +import { LuciaError } from "lucia"; -testAdapter(adapter, queryHandler); +import { connection } from "./db.js"; +import { helper, escapeName } from "../../src/utils.js"; +import { + getAll, + planetscaleAdapter, + transformPlanetscaleSession +} from "../../src/drivers/planetscale.js"; +import { TABLE_NAMES, ESCAPED_SESSION_TABLE_NAME } from "../shared.js"; + +import type { QueryHandler, TableQueryHandler } from "@lucia-auth/adapter-test"; +import type { PlanetscaleSession } from "../../src/drivers/planetscale.js"; + +const createTableQueryHandler = (tableName: string): TableQueryHandler => { + const ESCAPED_TABLE_NAME = escapeName(tableName); + return { + get: async () => { + return await getAll( + connection.execute(`SELECT * FROM ${ESCAPED_TABLE_NAME}`) + ); + }, + insert: async (value: any) => { + const [fields, placeholders, args] = helper(value); + await connection.execute( + `INSERT INTO ${ESCAPED_TABLE_NAME} ( ${fields} ) VALUES ( ${placeholders} )`, + args + ); + }, + clear: async () => { + await connection.execute(`DELETE FROM ${ESCAPED_TABLE_NAME}`); + } + }; +}; + +const queryHandler: QueryHandler = { + user: createTableQueryHandler(TABLE_NAMES.user), + session: { + ...createTableQueryHandler(TABLE_NAMES.session), + get: async () => { + const result = await getAll( + connection.execute(`SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME}`) + ); + return result.map((val) => transformPlanetscaleSession(val)); + } + }, + key: createTableQueryHandler(TABLE_NAMES.key) +}; + +const adapter = planetscaleAdapter(connection, TABLE_NAMES)(LuciaError); + +await testAdapter(adapter, new Database(queryHandler)); + +process.exit(0); diff --git a/packages/adapter-mysql/test/planetscale/setup.ts b/packages/adapter-mysql/test/planetscale/setup.ts new file mode 100644 index 000000000..19206d889 --- /dev/null +++ b/packages/adapter-mysql/test/planetscale/setup.ts @@ -0,0 +1,33 @@ +import { connection } from "./db.js"; +import { + ESCAPED_USER_TABLE_NAME, + ESCAPED_SESSION_TABLE_NAME, + ESCAPED_KEY_TABLE_NAME +} from "../shared.js"; + +await connection.execute(` +CREATE TABLE IF NOT EXISTS ${ESCAPED_USER_TABLE_NAME} ( + id VARCHAR(15) PRIMARY KEY, + username VARCHAR(15) NOT NULL UNIQUE +) +`); + +await connection.execute(` +CREATE TABLE IF NOT EXISTS ${ESCAPED_SESSION_TABLE_NAME} ( + id VARCHAR(127) PRIMARY KEY, + user_id VARCHAR(15) NOT NULL, + active_expires BIGINT UNSIGNED NOT NULL, + idle_expires BIGINT UNSIGNED NOT NULL, + country VARCHAR(2) NOT NULL +) +`); + +await connection.execute(` +CREATE TABLE IF NOT EXISTS ${ESCAPED_KEY_TABLE_NAME} ( + id VARCHAR(255) PRIMARY KEY, + user_id VARCHAR(15) NOT NULL, + hashed_password VARCHAR(255) +) +`); + +process.exit(0); diff --git a/packages/adapter-mysql/test/shared.ts b/packages/adapter-mysql/test/shared.ts new file mode 100644 index 000000000..b745122fc --- /dev/null +++ b/packages/adapter-mysql/test/shared.ts @@ -0,0 +1,11 @@ +import { escapeName } from "../src/utils"; + +export const TABLE_NAMES = { + user: "test_user", + session: "user_session", + key: "user_key" +}; + +export const ESCAPED_USER_TABLE_NAME = escapeName(TABLE_NAMES.user); +export const ESCAPED_SESSION_TABLE_NAME = escapeName(TABLE_NAMES.session); +export const ESCAPED_KEY_TABLE_NAME = escapeName(TABLE_NAMES.key); diff --git a/packages/adapter-postgresql/.env.example b/packages/adapter-postgresql/.env.example new file mode 100644 index 000000000..203c24137 --- /dev/null +++ b/packages/adapter-postgresql/.env.example @@ -0,0 +1 @@ +PSQL_DATABASE_URL="" \ No newline at end of file diff --git a/packages/adapter-postgresql/.gitignore b/packages/adapter-postgresql/.gitignore index 27a3b5d40..7a0ae98eb 100644 --- a/packages/adapter-postgresql/.gitignore +++ b/packages/adapter-postgresql/.gitignore @@ -1,7 +1,5 @@ /node_modules /dist .DS_Store -/prisma/migrations -/prisma/dev.db -/prisma/dev.db-journal .env +*.tgz \ No newline at end of file diff --git a/packages/adapter-postgresql/.npmignore b/packages/adapter-postgresql/.npmignore deleted file mode 100644 index e1ac86668..000000000 --- a/packages/adapter-postgresql/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -/node_modules -.DS_Store -/src -/tsconfig.json -.gitignore -.env -/prisma -/test \ No newline at end of file diff --git a/packages/adapter-postgresql/package.json b/packages/adapter-postgresql/package.json index 73aa43635..bd1776b34 100644 --- a/packages/adapter-postgresql/package.json +++ b/packages/adapter-postgresql/package.json @@ -2,21 +2,23 @@ "name": "@lucia-auth/adapter-postgresql", "version": "1.0.1", "description": "PostgreSQL adapter for Lucia", - "main": "index.js", - "types": "index.d.ts", - "module": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "module": "dist/index.js", "type": "module", "files": [ - "**/*" + "/dist/", + "CHANGELOG.md" ], "scripts": { - "build": "shx rm -rf ./dist/* && tsc && shx cp ./package.json ./dist && shx cp ./README.md ./dist && shx cp .npmignore dist", - "test.pg": " tsx test/pg/index.ts", - "auri.publish": "pnpm build && cd dist && pnpm install --no-frozen-lockfile && pnpm publish --no-git-checks --access public && cd ../" + "build": "shx rm -rf ./dist/* && tsc", + "test.pg": "tsx test/pg/index.ts", + "test-setup.pg": "tsx test/pg/setup.ts", + "auri.publish": "pnpm build && pnpm publish --no-git-checks --access public" }, "keywords": [ "lucia", - "lucia-auth", + "lucia", "auth", "pg", "postgresql", @@ -29,15 +31,15 @@ "repository": { "type": "git", "url": "https://github.com/pilcrowOnPaper/lucia", - "directory": "packages/adapter-prisma" + "directory": "packages/adapter-postgresql" }, "author": "pilcrowonpaper", "license": "MIT", "exports": { - ".": "./index.js" + ".": "./dist/index.js" }, "peerDependencies": { - "lucia-auth": "^1.4.0", + "lucia": "^2.0.0", "pg": "^8.0.0" }, "peerDependenciesMeta": { @@ -46,10 +48,10 @@ } }, "devDependencies": { - "@lucia-auth/adapter-test": "workspace:*", + "@lucia-auth/adapter-test": "latest", "@types/pg": "^8.6.5", "dotenv": "^16.0.3", - "lucia-auth": "workspace:*", + "lucia": "latest", "tsx": "^3.12.6" }, "dependencies": { diff --git a/packages/adapter-postgresql/src/core.ts b/packages/adapter-postgresql/src/core.ts deleted file mode 100644 index 4d7893f6a..000000000 --- a/packages/adapter-postgresql/src/core.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { transformDatabaseSession, transformDatabaseKey } from "./utils.js"; - -import type { - PostgresKeySchema, - PostgresUserSchema, - PostgresSessionSchema -} from "./utils.js"; -import { Adapter, KeySchema, SessionSchema } from "lucia-auth"; -import type { ColumnValue, Operator } from "./query.js"; - -export const createCoreAdapter = (operator: Operator) => { - const helper = createQueryHelper(operator); - return { - getUser: async (userId) => { - return await helper.getUser(userId); - }, - getSessionAndUserBySessionId: async (sessionId) => { - const data = await operator.get< - PostgresUserSchema & { - _session_active_expires: number; - _session_id: string; - _session_idle_expires: number; - _session_user_id: string; - } - >((ctx) => [ - ctx.selectFrom( - "auth_session", - "auth_user.*", - "auth_session.id as _session_id", - "auth_session.active_expires as _session_active_expires", - "auth_session.idle_expires as _session_idle_expires", - "auth_session.user_id as _session_user_id" - ), - ctx.innerJoin("auth_user", "auth_user.id", "auth_session.user_id"), - ctx.where("auth_session.id", "=", sessionId) - ]); - if (!data) return null; - const { - _session_active_expires, - _session_id, - _session_idle_expires, - _session_user_id, - ...user - } = data; - return { - user, - session: transformDatabaseSession({ - id: _session_id, - user_id: _session_user_id, - active_expires: _session_active_expires, - idle_expires: _session_idle_expires - }) - }; - }, - getSession: async (sessionId) => { - const databaseSession = await operator.get( - (ctx) => [ - ctx.selectFrom("auth_session", "*"), - ctx.where("id", "=", sessionId) - ] - ); - if (!databaseSession) return null; - return transformDatabaseSession(databaseSession); - }, - getSessionsByUserId: async (userId) => { - const databaseSessions = await operator.getAll( - (ctx) => [ - ctx.selectFrom("auth_session", "*"), - ctx.where("user_id", "=", userId) - ] - ); - return databaseSessions.map((val) => transformDatabaseSession(val)); - }, - deleteUser: async (userId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_user"), - ctx.where("id", "=", userId) - ]); - }, - setSession: async (session) => { - await helper.insertSession(session); - }, - deleteSession: async (sessionId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_session"), - ctx.where("id", "=", sessionId) - ]); - }, - deleteSessionsByUserId: async (userId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_session"), - ctx.where("user_id", "=", userId) - ]); - }, - setKey: async (key) => { - await helper.insertKey(key); - }, - getKey: async (keyId) => { - const databaseKey = await operator.get((ctx) => [ - ctx.selectFrom("auth_key", "*"), - ctx.where("id", "=", keyId) - ]); - if (!databaseKey) return null; - const transformedDatabaseKey = transformDatabaseKey(databaseKey); - return transformedDatabaseKey; - }, - getKeysByUserId: async (userId) => { - const databaseKeys = await operator.getAll((ctx) => [ - ctx.selectFrom("auth_key", "*"), - ctx.where("user_id", "=", userId) - ]); - return databaseKeys.map((val) => transformDatabaseKey(val)); - }, - deleteKeysByUserId: async (userId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_key"), - ctx.where("user_id", "=", userId) - ]); - }, - deleteNonPrimaryKey: async (keyId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_key"), - ctx.and( - ctx.where("id", "=", keyId), - ctx.where("primary_key", "=", false) - ) - ]); - } - } satisfies Partial; -}; - -export const createQueryHelper = (operator: Operator) => { - return { - getUser: async (userId: string) => { - return await operator.get((ctx) => [ - ctx.selectFrom("auth_user", "*"), - ctx.where("id", "=", userId) - ]); - }, - updateUserAttributes: async ( - userId: string, - attributes: Record - ) => { - return await operator.get((ctx) => [ - ctx.update("auth_user", attributes), - ctx.where("id", "=", userId), - ctx.returning("*") - ]); - }, - updateKeyPassword: async (keyId: string, hashedPassword: string | null) => { - const databaseKey = await operator.get((ctx) => [ - ctx.update("auth_key", { - hashed_password: hashedPassword - }), - ctx.where("id", "=", keyId), - ctx.returning("*") - ]); - if (!databaseKey) return null; - return transformDatabaseKey(databaseKey); - }, - insertUser: async ( - userId: string, - attributes: Record - ) => { - const user = { - id: userId, - ...attributes - }; - return await operator.get((ctx) => [ - ctx.insertInto("auth_user", user), - ctx.returning("*") - ]); - }, - insertSession: async (session: SessionSchema) => { - await operator.run((ctx) => [ctx.insertInto("auth_session", session)]); - }, - insertKey: async (key: KeySchema) => { - await operator.run((ctx) => [ctx.insertInto("auth_key", key)]); - } - }; -}; diff --git a/packages/adapter-postgresql/src/drivers/pg.ts b/packages/adapter-postgresql/src/drivers/pg.ts new file mode 100644 index 000000000..5c52ff403 --- /dev/null +++ b/packages/adapter-postgresql/src/drivers/pg.ts @@ -0,0 +1,280 @@ +import { helper, getSetArgs, escapeName } from "../utils.js"; + +import type { + Adapter, + InitializeAdapter, + UserSchema, + SessionSchema, + KeySchema +} from "lucia"; +import type { + QueryResult, + DatabaseError, + Pool, + PoolClient, + QueryResultRow +} from "pg"; + +export const pgAdapter = ( + pool: Pool, + tables: { + user: string; + session: string; + key: string; + } +): InitializeAdapter => { + const transaction = async ( + execute: (connection: PoolClient) => Promise + ): Promise => { + const connection = await pool.connect(); + try { + await connection.query("BEGIN"); + await execute(connection); + await connection.query("COMMIT"); + } catch (e) { + connection.query("ROLLBACK"); + throw e; + } + }; + + const ESCAPED_USER_TABLE_NAME = escapeName(tables.user); + const ESCAPED_SESSION_TABLE_NAME = escapeName(tables.session); + const ESCAPED_KEY_TABLE_NAME = escapeName(tables.key); + + return (LuciaError) => { + return { + getUser: async (userId) => { + const result = await get( + pool.query(`SELECT * FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = $1`, [ + userId + ]) + ); + return result; + }, + setUser: async (user, key) => { + if (!key) { + const [userFields, userValues, userArgs] = helper(user); + await pool.query( + `INSERT INTO ${ESCAPED_USER_TABLE_NAME} ( ${userFields} ) VALUES ( ${userValues} )`, + userArgs + ); + return; + } + try { + await transaction(async (tx) => { + const [userFields, userValues, userArgs] = helper(user); + await tx.query( + `INSERT INTO ${ESCAPED_USER_TABLE_NAME} ( ${userFields} ) VALUES ( ${userValues} )`, + userArgs + ); + const [keyFields, keyValues, keyArgs] = helper(key); + await tx.query( + `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${keyFields} ) VALUES ( ${keyValues} )`, + keyArgs + ); + }); + } catch (e) { + const error = e as Partial; + if (error.code === "23505" && error.detail?.includes("Key (id)")) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw e; + } + }, + deleteUser: async (userId) => { + await pool.query( + `DELETE FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = $1`, + [userId] + ); + }, + updateUser: async (userId, partialUser) => { + const [fields, values, args] = helper(partialUser); + await pool.query( + `UPDATE ${ESCAPED_USER_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = $${fields.length + 1}`, + [...args, userId] + ); + }, + + getSession: async (sessionId) => { + const result = await get( + pool.query( + `SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = $1`, + [sessionId] + ) + ); + return result ? transformPgSession(result) : null; + }, + getSessionsByUserId: async (userId) => { + const result = await getAll( + pool.query( + `SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = $1`, + [userId] + ) + ); + return result.map((val) => transformPgSession(val)); + }, + setSession: async (session) => { + try { + const [fields, values, args] = helper(session); + await pool.query( + `INSERT INTO ${ESCAPED_SESSION_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )`, + args + ); + } catch (e) { + const error = e as Partial; + if ( + error.code === "23503" && + error.detail?.includes("Key (user_id)") + ) { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + throw e; + } + }, + deleteSession: async (sessionId) => { + await pool.query( + `DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = $1`, + [sessionId] + ); + }, + deleteSessionsByUserId: async (userId) => { + await pool.query( + `DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = $1`, + [userId] + ); + }, + updateSession: async (sessionId, partialSession) => { + const [fields, values, args] = helper(partialSession); + await pool.query( + `UPDATE ${ESCAPED_SESSION_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = $${fields.length + 1}`, + [...args, sessionId] + ); + }, + + getKey: async (keyId) => { + const result = await get( + pool.query( + `SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = $1`, + [keyId] + ) + ); + return result; + }, + getKeysByUserId: async (userId) => { + const result = getAll( + pool.query( + `SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = $1`, + [userId] + ) + ); + return result; + }, + setKey: async (key) => { + try { + const [fields, values, args] = helper(key); + await pool.query( + `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )`, + args + ); + } catch (e) { + const error = e as Partial; + if ( + error.code === "23503" && + error.detail?.includes("Key (user_id)") + ) { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + if (error.code === "23505" && error.detail?.includes("Key (id)")) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw e; + } + }, + deleteKey: async (keyId) => { + await pool.query( + `DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = $1`, + [keyId] + ); + }, + deleteKeysByUserId: async (userId) => { + await pool.query( + `DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = $1`, + [userId] + ); + }, + updateKey: async (keyId, partialKey) => { + const [fields, values, args] = helper(partialKey); + await pool.query( + `UPDATE ${ESCAPED_KEY_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = $${fields.length + 1}`, + [...args, keyId] + ); + }, + + getSessionAndUser: async (sessionId) => { + const getSessionPromise = get( + pool.query( + `SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = $1`, + [sessionId] + ) + ); + const getUserFromJoinPromise = get( + pool.query< + UserSchema & { + __session_id: string; + } + >( + `SELECT ${ESCAPED_USER_TABLE_NAME}.*, ${ESCAPED_SESSION_TABLE_NAME}.id as __session_id FROM ${ESCAPED_SESSION_TABLE_NAME} INNER JOIN ${ESCAPED_USER_TABLE_NAME} ON ${ESCAPED_USER_TABLE_NAME}.id = ${ESCAPED_SESSION_TABLE_NAME}.user_id WHERE ${ESCAPED_SESSION_TABLE_NAME}.id = $1`, + [sessionId] + ) + ); + const [sessionResult, userFromJoinResult] = await Promise.all([ + getSessionPromise, + getUserFromJoinPromise + ]); + if (!sessionResult || !userFromJoinResult) return [null, null]; + const { __session_id: _, ...userResult } = userFromJoinResult; + return [transformPgSession(sessionResult), userResult]; + } + }; + }; +}; + +export const get = async <_Schema extends QueryResultRow>( + queryPromise: Promise> +): Promise<_Schema | null> => { + const { rows } = await queryPromise; + const result = rows.at(0) ?? null; + return result; +}; + +export const getAll = async <_Schema extends QueryResultRow>( + queryPromise: Promise> +): Promise<_Schema[]> => { + const { rows } = await queryPromise; + return rows; +}; + +export type PgSession = Omit< + SessionSchema, + "active_expires" | "idle_expires" +> & { + active_expires: BigInt; + idle_expires: BigInt; +}; + +export const transformPgSession = (session: PgSession): SessionSchema => { + return { + ...session, + active_expires: Number(session.active_expires), + idle_expires: Number(session.idle_expires) + }; +}; diff --git a/packages/adapter-postgresql/src/index.ts b/packages/adapter-postgresql/src/index.ts index 2f04c80cb..ae5e531ae 100644 --- a/packages/adapter-postgresql/src/index.ts +++ b/packages/adapter-postgresql/src/index.ts @@ -1 +1 @@ -export { pgAdapter as pg } from "./pg/index.js"; +export { pgAdapter as pg } from "./drivers/pg.js"; diff --git a/packages/adapter-postgresql/src/lucia.d.ts b/packages/adapter-postgresql/src/lucia.d.ts index f61de1891..8026ca988 100644 --- a/packages/adapter-postgresql/src/lucia.d.ts +++ b/packages/adapter-postgresql/src/lucia.d.ts @@ -1,5 +1,6 @@ -/// +/// declare namespace Lucia { type Auth = any; - type UserAttributes = {}; + type DatabaseUserAttributes = any; + type DatabaseSessionAttributes = any; } diff --git a/packages/adapter-postgresql/src/pg/index.ts b/packages/adapter-postgresql/src/pg/index.ts deleted file mode 100644 index 00a51db76..000000000 --- a/packages/adapter-postgresql/src/pg/index.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { createOperator } from "../query.js"; -import { pgRunner } from "./runner.js"; -import { createCoreAdapter, createQueryHelper } from "../core.js"; - -import type { Adapter, AdapterFunction } from "lucia-auth"; -import type { Pool, DatabaseError } from "./types.js"; - -export const pgAdapter = (pool: Pool): AdapterFunction => { - const transaction = async <_Execute extends () => Promise>( - execute: _Execute - ): Promise>> => { - const connection = await pool.connect(); - try { - await connection.query("BEGIN"); - const result = await execute(); - await connection.query("COMMIT"); - return result; - } catch (e) { - connection.query("ROLLBACK"); - throw e; - } - }; - - return (LuciaError) => { - const operator = createOperator(pgRunner(pool)); - const coreAdapter = createCoreAdapter(operator); - const helper = createQueryHelper(operator); - return { - ...coreAdapter, - setUser: async (userId, attributes, key) => { - try { - if (key) { - const insertedUser = await transaction(async () => { - const databaseUser = await helper.insertUser(userId, attributes); - if (!databaseUser) throw new TypeError("Unexpected query result"); - await helper.insertKey(key); - return databaseUser; - }); - return insertedUser; - } - const insertedUser = await helper.insertUser(userId, attributes); - if (!insertedUser) throw new TypeError("Unexpected type"); - return insertedUser; - } catch (e) { - const error = e as Partial; - if (error.code === "23505" && error.detail?.includes("Key (id)")) { - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - throw e; - } - }, - setSession: async (session) => { - try { - await helper.insertSession(session); - } catch (e) { - const error = e as Partial; - if ( - error.code === "23503" && - error.detail?.includes("Key (user_id)") - ) { - throw new LuciaError("AUTH_INVALID_USER_ID"); - } - if (error.code === "23505" && error.detail?.includes("Key (id)")) { - throw new LuciaError("AUTH_DUPLICATE_SESSION_ID"); - } - throw e; - } - }, - updateUserAttributes: async (userId, attributes) => { - if (Object.keys(attributes).length === 0) { - const databaseUser = await helper.getUser(userId); - if (!databaseUser) throw new LuciaError("AUTH_INVALID_USER_ID"); - return databaseUser; - } - const updatedUser = await helper.updateUserAttributes( - userId, - attributes - ); - if (!updatedUser) throw new LuciaError("AUTH_INVALID_USER_ID"); - return updatedUser; - }, - setKey: async (key) => { - try { - await helper.insertKey(key); - } catch (e) { - const error = e as Partial; - if ( - error.code === "23503" && - error.detail?.includes("Key (user_id)") - ) { - throw new LuciaError("AUTH_INVALID_USER_ID"); - } - if (error.code === "23505" && error.detail?.includes("Key (id)")) { - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - throw error; - } - }, - updateKeyPassword: async (keyId, hashedPassword) => { - const updatedKey = await helper.updateKeyPassword( - keyId, - hashedPassword - ); - if (!updatedKey) throw new LuciaError("AUTH_INVALID_KEY_ID"); - return updatedKey; - } - }; - }; -}; diff --git a/packages/adapter-postgresql/src/pg/runner.ts b/packages/adapter-postgresql/src/pg/runner.ts deleted file mode 100644 index 770c184bb..000000000 --- a/packages/adapter-postgresql/src/pg/runner.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Runner } from "../query.js"; -import type { Pool } from "./types.js"; - -export const pgRunner = (pool: Pool): Runner => { - return { - get: async (query, params) => { - const result = await pool.query(query, params); - return result.rows; - }, - run: async (query, params) => { - await pool.query(query, params); - } - }; -}; diff --git a/packages/adapter-postgresql/src/pg/types.ts b/packages/adapter-postgresql/src/pg/types.ts deleted file mode 100644 index 047493fc4..000000000 --- a/packages/adapter-postgresql/src/pg/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -import pg from "pg"; - -export type Pool = InstanceType; -export type DatabaseError = InstanceType; diff --git a/packages/adapter-postgresql/src/query.ts b/packages/adapter-postgresql/src/query.ts deleted file mode 100644 index 0b2b9652c..000000000 --- a/packages/adapter-postgresql/src/query.ts +++ /dev/null @@ -1,261 +0,0 @@ -const resolveQueryBlock = (block: Block, paramCount: number): ResolvedBlock => { - const escapeName = (val: string) => { - if (val === "*") return val; - return `"${val}"`; - }; - if (block.type === "DELETE_FROM") { - return { - queryChunk: `DELETE FROM ${escapeName(block.table)}`, - params: [] - }; - } - if (block.type === "INNER_JOIN") { - return { - queryChunk: `INNER JOIN ${escapeName(block.targetTable)} ON ${ - block.targetColumn - } = ${block.column}`, - params: [] - }; - } - if (block.type === "INSERT_INTO") { - const keys = Object.keys(block.values); - return { - queryChunk: `INSERT INTO ${escapeName(block.table)} (${keys.map((k) => - escapeName(k) - )}) VALUES (${Array(keys.length) - .fill("") - .map((_, i) => `$${paramCount + i + 1}`)})`, - params: keys.map((k) => block.values[k]) - }; - } - if (block.type === "RETURNING") { - return { - queryChunk: `RETURNING ${block.columns}`, - params: [] - }; - } - if (block.type === "SELECT") { - return { - queryChunk: `SELECT ${block.columns} FROM ${escapeName(block.table)}`, - params: [] - }; - } - if (block.type === "WHERE") { - return { - queryChunk: `WHERE ${block.column} ${block.comparator} $${ - paramCount + 1 - }`, - params: [block.value] - }; - } - if (block.type === "UPDATE") { - const keys = Object.keys(block.values); - return { - queryChunk: `UPDATE ${escapeName(block.table)} SET ${keys.map((k, i) => { - return `${escapeName(k)} = $${paramCount + i + 1}`; - })}`, - params: keys.map((k) => block.values[k]) - }; - } - if (block.type === "AND") { - const resolvedConditionQueryBlocks = block.whereBlocks.map( - (whereBlock, i) => { - return { - queryChunk: `${whereBlock.column} ${whereBlock.comparator} $${ - paramCount + 1 + i - }`, - params: [whereBlock.value] - }; - } - ); - const conditionQueryChunk = resolvedConditionQueryBlocks - .map((resolvedBlock) => resolvedBlock.queryChunk) - .join(" AND "); - return { - queryChunk: `WHERE ${conditionQueryChunk}`, - params: resolvedConditionQueryBlocks.reduce( - (acc, curr) => [...acc, ...curr.params], - [] as ColumnValue[] - ) - }; - } - throw new TypeError(`Invalid block type`); -}; - -const ctx = { - innerJoin: (targetTable: string, targetColumn: string, column: string) => { - return { - type: "INNER_JOIN", - targetTable, - targetColumn, - column - }; - }, - selectFrom: (table: string, ...columns: [string, ...string[]]) => { - return { - type: "SELECT", - table, - columns - }; - }, - returning: (...columns: [string, ...string[]]) => { - return { - type: "RETURNING", - columns - }; - }, - insertInto: (table: string, values: Record) => { - return { - type: "INSERT_INTO", - table, - values - }; - }, - where: (column: string, comparator: string, value: ColumnValue) => { - return { - type: "WHERE", - column, - comparator, - value - }; - }, - deleteFrom: (table: string) => { - return { - type: "DELETE_FROM", - table - }; - }, - update: (table: string, values: Record) => { - return { - type: "UPDATE", - table, - values - }; - }, - and: (...whereBlocks: WhereBlock[]) => { - return { - type: "AND", - whereBlocks - }; - } -} satisfies Record Block>; - -export const createOperator = <_Runner extends Runner>(runner: _Runner) => { - const resolveQueryBlocks = (queryBlocks: Block[]) => { - const queryChunks: string[] = []; - const params: ColumnValue[] = []; - for (const queryBlock of queryBlocks) { - const resolvedBlock = resolveQueryBlock(queryBlock, params.length); - queryChunks.push(resolvedBlock.queryChunk); - params.push(...resolvedBlock.params); - } - const statement = queryChunks.join(" "); - return { - statement, - params - }; - }; - - const write = <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ) => { - const blocks = createQueryBlocks(ctx); - return resolveQueryBlocks(blocks); - }; - const get = async <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ): Promise<_Selection | null> => { - const query = write(createQueryBlocks); - const result = await runner.get(query.statement, query.params); - if (Array.isArray(result)) return result.at(0) ?? null; - return result ?? null; - }; - const getAll = async <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ): Promise<_Selection[]> => { - const query = write(createQueryBlocks); - const result = await runner.get(query.statement, query.params); - if (!result) return [] as any; - if (!Array.isArray(result)) return [result] as any; - return result as any; - }; - const run = <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ): Promise => { - const query = write(createQueryBlocks); - return runner.run(query.statement, query.params) as any; - }; - return { - write, - get, - getAll, - run - } as const; -}; - -export type Operator = ReturnType; - -export type Context = typeof ctx; - -type CreateQueryBlocks = (context: Context) => Block[]; - -type ResolvedBlock = { - queryChunk: string; - params: ColumnValue[]; -}; - -export type Runner = { - get: (statement: string, params: ColumnValue[]) => Promise; - run: (statement: string, params: ColumnValue[]) => Promise; -}; - -export type ColumnValue = string | number | null | bigint | boolean; - -type Block = - | { - type: "INNER_JOIN"; - targetTable: string; - targetColumn: string; - column: string; - } - | { - type: "SELECT"; - table: string; - columns: string[]; - } - | { - type: "RETURNING"; - columns: string[]; - } - | { - type: "INSERT_INTO"; - table: string; - values: Record; - } - | { - type: "WHERE"; - column: string; - comparator: string; - value: ColumnValue; - } - | { - type: "AND"; - whereBlocks: WhereBlock[]; - } - | { - type: "DELETE_FROM"; - table: string; - } - | { - type: "UPDATE"; - table: string; - values: Record; - } - | WhereBlock; - -type WhereBlock = { - type: "WHERE"; - column: string; - comparator: string; - value: ColumnValue; -}; diff --git a/packages/adapter-postgresql/src/utils.ts b/packages/adapter-postgresql/src/utils.ts index 5f7be7ae0..e28415a7e 100644 --- a/packages/adapter-postgresql/src/utils.ts +++ b/packages/adapter-postgresql/src/utils.ts @@ -1,33 +1,29 @@ -import type { SessionSchema, UserSchema, KeySchema } from "lucia-auth"; -import { ColumnValue } from "./query.js"; - -export const transformDatabaseSession = ( - session: PostgresSessionSchema -): SessionSchema => { - return { - id: session.id, - user_id: session.user_id, - active_expires: Number(session.active_expires), - idle_expires: Number(session.idle_expires) +const createPreparedStatementHelper = ( + placeholder: (index: number) => string +) => { + const helper = ( + values: Record + ): readonly [fields: string[], placeholders: string[], arguments: any[]] => { + const keys = Object.keys(values); + return [ + keys.map((k) => escapeName(k)), + keys.map((_, i) => placeholder(i)), + keys.map((k) => values[k]) + ] as const; }; + return helper; }; -export const transformDatabaseKey = (key: PostgresKeySchema): KeySchema => { - return { - id: key.id, - user_id: key.user_id, - primary_key: Boolean(key.primary_key), - hashed_password: key.hashed_password, - expires: key.expires === null ? null : Number(key.expires) - }; -}; +const ESCAPE_CHAR = `"`; -type PgSchema<_Schema extends Record> = { - [K in keyof _Schema]: Extract<_Schema[K], number> extends never - ? _Schema[K] - : _Schema[K] | string; +export const escapeName = (val: string) => { + return `${ESCAPE_CHAR}${val}${ESCAPE_CHAR}`; }; -export type PostgresSessionSchema = PgSchema; -export type PostgresKeySchema = PgSchema; -export type PostgresUserSchema = PgSchema; +export const helper = createPreparedStatementHelper((i) => `$${i + 1}`); + +export const getSetArgs = (fields: string[], placeholders: string[]) => { + return fields + .map((field, i) => [field, placeholders[i]].join(" = ")) + .join(","); +}; diff --git a/packages/adapter-postgresql/test/index.ts b/packages/adapter-postgresql/test/index.ts deleted file mode 100644 index a808b47be..000000000 --- a/packages/adapter-postgresql/test/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { LuciaQueryHandler } from "@lucia-auth/adapter-test"; -import { Runner, createOperator } from "../src/query.js"; -import { - transformDatabaseKey, - transformDatabaseSession -} from "../src/utils.js"; - -import type { PostgresKeySchema, PostgresSessionSchema } from "../src/utils.js"; -import type { TestUserSchema } from "@lucia-auth/adapter-test"; - -export const createQueryHandler = (runner: Runner) => { - const operator = createOperator(runner); - return { - user: { - get: async () => { - return await operator.getAll((ctx) => [ - ctx.selectFrom("auth_user", "*") - ]); - }, - insert: async (user) => { - await operator.run((ctx) => [ctx.insertInto("auth_user", user)]); - }, - clear: async () => { - await operator.run((ctx) => [ctx.deleteFrom("auth_user")]); - } - }, - session: { - get: async () => { - const result = await operator.getAll((ctx) => [ - ctx.selectFrom("auth_session", "*") - ]); - return result.map((val) => transformDatabaseSession(val)); - }, - insert: async (key) => { - await operator.run((ctx) => [ctx.insertInto("auth_session", key)]); - }, - clear: async () => { - await operator.run((ctx) => [ctx.deleteFrom("auth_session")]); - } - }, - key: { - get: async () => { - const result = await operator.getAll((ctx) => [ - ctx.selectFrom("auth_key", "*") - ]); - return result.map((val) => transformDatabaseKey(val)); - }, - insert: async (key) => { - await operator.run((ctx) => [ctx.insertInto("auth_key", key)]); - }, - clear: async () => { - await operator.run((ctx) => [ctx.deleteFrom("auth_key")]); - } - } - } satisfies LuciaQueryHandler; -}; diff --git a/packages/adapter-postgresql/test/pg/db.ts b/packages/adapter-postgresql/test/pg/db.ts index ec1d05329..e68ff2f66 100644 --- a/packages/adapter-postgresql/test/pg/db.ts +++ b/packages/adapter-postgresql/test/pg/db.ts @@ -1,19 +1,11 @@ import dotenv from "dotenv"; import { resolve } from "path"; -import { LuciaError } from "lucia-auth"; - -import { pgAdapter } from "../../src/pg/index.js"; -import { pgRunner } from "../../src/pg/runner.js"; import pg from "pg"; -import { createQueryHandler } from "../index.js"; dotenv.config({ path: `${resolve()}/.env` }); -const pool = new pg.Pool({ +export const pool = new pg.Pool({ connectionString: process.env.PSQL_DATABASE_URL }); - -export const adapter = pgAdapter(pool)(LuciaError); -export const queryHandler = createQueryHandler(pgRunner(pool)); diff --git a/packages/adapter-postgresql/test/pg/index.ts b/packages/adapter-postgresql/test/pg/index.ts index ee388e4f5..dd2578cdf 100644 --- a/packages/adapter-postgresql/test/pg/index.ts +++ b/packages/adapter-postgresql/test/pg/index.ts @@ -1,4 +1,49 @@ -import { testAdapter } from "@lucia-auth/adapter-test"; -import { adapter, queryHandler } from "./db.js"; +import { testAdapter, Database } from "@lucia-auth/adapter-test"; +import { LuciaError } from "lucia"; -testAdapter(adapter, queryHandler); +import { pool } from "./db.js"; +import { escapeName, helper } from "../../src/utils.js"; +import { getAll, pgAdapter, transformPgSession } from "../../src/drivers/pg.js"; +import { ESCAPED_SESSION_TABLE_NAME, TABLE_NAMES } from "../shared.js"; + +import type { QueryHandler, TableQueryHandler } from "@lucia-auth/adapter-test"; +import type { PgSession } from "../../src/drivers/pg.js"; + +const createTableQueryHandler = (tableName: string): TableQueryHandler => { + const ESCAPED_TABLE_NAME = escapeName(tableName); + return { + get: async () => { + return await getAll(pool.query(`SELECT * FROM ${ESCAPED_TABLE_NAME}`)); + }, + insert: async (value: any) => { + const [fields, placeholders, args] = helper(value); + await pool.query( + `INSERT INTO ${ESCAPED_TABLE_NAME} ( ${fields} ) VALUES ( ${placeholders} )`, + args + ); + }, + clear: async () => { + await pool.query(`DELETE FROM ${ESCAPED_TABLE_NAME}`); + } + }; +}; + +const queryHandler: QueryHandler = { + user: createTableQueryHandler(TABLE_NAMES.user), + session: { + ...createTableQueryHandler(TABLE_NAMES.session), + get: async () => { + const result = await getAll( + pool.query(`SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME}`) + ); + return result.map((val) => transformPgSession(val)); + } + }, + key: createTableQueryHandler(TABLE_NAMES.key) +}; + +const adapter = pgAdapter(pool, TABLE_NAMES)(LuciaError); + +await testAdapter(adapter, new Database(queryHandler)); + +process.exit(0); diff --git a/packages/adapter-postgresql/test/pg/setup.ts b/packages/adapter-postgresql/test/pg/setup.ts new file mode 100644 index 000000000..923868311 --- /dev/null +++ b/packages/adapter-postgresql/test/pg/setup.ts @@ -0,0 +1,33 @@ +import { + ESCAPED_KEY_TABLE_NAME, + ESCAPED_SESSION_TABLE_NAME, + ESCAPED_USER_TABLE_NAME +} from "../shared.js"; +import { pool } from "./db.js"; + +await pool.query(` +CREATE TABLE ${ESCAPED_USER_TABLE_NAME} ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL UNIQUE +) +`); + +await pool.query(` +CREATE TABLE ${ESCAPED_SESSION_TABLE_NAME} ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES ${ESCAPED_USER_TABLE_NAME}(id), + active_expires BIGINT NOT NULL, + idle_expires BIGINT NOT NULL, + country TEXT NOT NULL +) +`); + +await pool.query(` +CREATE TABLE ${ESCAPED_KEY_TABLE_NAME} ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES ${ESCAPED_USER_TABLE_NAME}(id), + hashed_password VARCHAR(255) +) +`); + +process.exit(0); diff --git a/packages/adapter-postgresql/test/shared.ts b/packages/adapter-postgresql/test/shared.ts new file mode 100644 index 000000000..a13fb1dd1 --- /dev/null +++ b/packages/adapter-postgresql/test/shared.ts @@ -0,0 +1,11 @@ +import { escapeName } from "../src/utils.js"; + +export const TABLE_NAMES = { + user: "test_user", + session: "user_session", + key: "user_key" +}; + +export const ESCAPED_USER_TABLE_NAME = escapeName(TABLE_NAMES.user); +export const ESCAPED_SESSION_TABLE_NAME = escapeName(TABLE_NAMES.session); +export const ESCAPED_KEY_TABLE_NAME = escapeName(TABLE_NAMES.key); diff --git a/packages/adapter-prisma/.gitignore b/packages/adapter-prisma/.gitignore index 27a3b5d40..7a0ae98eb 100644 --- a/packages/adapter-prisma/.gitignore +++ b/packages/adapter-prisma/.gitignore @@ -1,7 +1,5 @@ /node_modules /dist .DS_Store -/prisma/migrations -/prisma/dev.db -/prisma/dev.db-journal .env +*.tgz \ No newline at end of file diff --git a/packages/adapter-prisma/.npmignore b/packages/adapter-prisma/.npmignore deleted file mode 100644 index e1ac86668..000000000 --- a/packages/adapter-prisma/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -/node_modules -.DS_Store -/src -/tsconfig.json -.gitignore -.env -/prisma -/test \ No newline at end of file diff --git a/packages/adapter-prisma/README.md b/packages/adapter-prisma/README.md index 3af843a4d..364329278 100644 --- a/packages/adapter-prisma/README.md +++ b/packages/adapter-prisma/README.md @@ -20,10 +20,7 @@ Requires `lucia-auth@0.11.0`. ## Testing -``` -pnpm exec prisma migrate dev --name init -``` - -``` +```bash +pnpm test-setup pnpm test ``` diff --git a/packages/adapter-prisma/package.json b/packages/adapter-prisma/package.json index 223c78c1b..e0867d99b 100644 --- a/packages/adapter-prisma/package.json +++ b/packages/adapter-prisma/package.json @@ -2,22 +2,24 @@ "name": "@lucia-auth/adapter-prisma", "version": "2.0.0", "description": "Prisma adapter for Lucia", - "main": "index.js", - "types": "index.d.ts", - "module": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "module": "dist/index.js", "type": "module", "files": [ - "**/*" + "/dist/", + "CHANGELOG.md" ], "scripts": { - "build": "shx rm -rf ./dist/* && tsc && shx cp ./package.json ./dist && shx cp ./README.md ./dist && shx cp .npmignore dist", + "build": "shx rm -rf ./dist/* && tsc", "test": "tsx test/index.ts", - "auri.publish": "pnpm build && cd dist && pnpm install --no-frozen-lockfile && pnpm publish --no-git-checks --access public && cd ../" + "test-setup": "prisma db push", + "auri.publish": "pnpm build && pnpm publish --no-git-checks --access public" }, "keywords": [ "lucia", "prisma", - "lucia-auth", + "lucia", "auth", "postgres", "mysql", @@ -35,15 +37,15 @@ "author": "pilcrowonpaper", "license": "MIT", "exports": { - ".": "./index.js" + ".": "./dist/index.js" }, "peerDependencies": { "@prisma/client": "^4.2.0", - "lucia-auth": "^1.3.0" + "lucia": "^2.0.0" }, "devDependencies": { - "lucia-auth": "workspace:*", - "@lucia-auth/adapter-test": "workspace:*", + "lucia": "latest", + "@lucia-auth/adapter-test": "latest", "@prisma/client": "^4.9.0", "prisma": "^4.9.0", "tsx": "^3.12.6" diff --git a/packages/adapter-sqlite/test/d1/.wrangler/state/d1/DB.sqlite3 b/packages/adapter-prisma/prisma/dev.db similarity index 57% rename from packages/adapter-sqlite/test/d1/.wrangler/state/d1/DB.sqlite3 rename to packages/adapter-prisma/prisma/dev.db index 3049118f4..91707f19a 100644 Binary files a/packages/adapter-sqlite/test/d1/.wrangler/state/d1/DB.sqlite3 and b/packages/adapter-prisma/prisma/dev.db differ diff --git a/packages/adapter-prisma/prisma/schema.prisma b/packages/adapter-prisma/prisma/schema.prisma index 5a849fe5c..235d6f168 100644 --- a/packages/adapter-prisma/prisma/schema.prisma +++ b/packages/adapter-prisma/prisma/schema.prisma @@ -10,34 +10,33 @@ datasource db { url = "file:./dev.db" } -model AuthUser { - id String @id @unique - username String @unique - auth_session AuthSession[] - auth_key AuthKey[] +model User { + id String @id @unique + username String @unique + auth_session Session[] + auth_key Key[] - @@map("auth_user") + @@map("test_user") } -model AuthSession { - id String @id @unique +model Session { + id String @id @unique user_id String active_expires BigInt idle_expires BigInt - auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade) + country String + test_user User @relation(references: [id], fields: [user_id], onDelete: Cascade) @@index([user_id]) - @@map("auth_session") + @@map("user_session") } -model AuthKey { - id String @id @unique +model Key { + id String @id @unique hashed_password String? user_id String - primary_key Boolean - expires BigInt? - auth_user AuthUser @relation(references: [id], fields: [user_id], onDelete: Cascade) + test_user User @relation(references: [id], fields: [user_id], onDelete: Cascade) @@index([user_id]) - @@map("auth_key") + @@map("user_key") } diff --git a/packages/adapter-prisma/src/index.ts b/packages/adapter-prisma/src/index.ts index fd5a9783c..2fbb7db38 100644 --- a/packages/adapter-prisma/src/index.ts +++ b/packages/adapter-prisma/src/index.ts @@ -1,226 +1 @@ -import type { - Adapter, - AdapterFunction, - KeySchema, - SessionSchema, - UserSchema -} from "lucia-auth"; -import { transformDatabaseKey, transformDatabaseSession } from "./utils.js"; -import { PrismaClient, SmartPrismaClient } from "./prisma.js"; - -interface PossiblePrismaError { - code: string; - message: string; -} - -type Models = { - authUser: { - schema: UserSchema; - relations: {}; - }; - authSession: { - schema: SessionSchema; - relations: { - auth_user: UserSchema; - }; - }; - authKey: { - schema: KeySchema; - relations: { - auth_user: UserSchema; - }; - }; -}; - -const adapter = - (prismaClient: PrismaClient): AdapterFunction => - (LuciaError) => { - const prisma = prismaClient as any as SmartPrismaClient; - return { - getUser: async (userId) => { - return await prisma.authUser.findUnique({ - where: { - id: userId - } - }); - }, - getSessionAndUserBySessionId: async (sessionId) => { - const data = await prisma.authSession.findUnique({ - where: { - id: sessionId - }, - include: { - auth_user: true - } - }); - if (!data) return null; - const { auth_user: user, ...session } = data; - return { - user, - session: transformDatabaseSession(session) - }; - }, - getSession: async (sessionId) => { - const session = await prisma.authSession.findUnique({ - where: { - id: sessionId - } - }); - if (!session) return null; - return transformDatabaseSession(session); - }, - getSessionsByUserId: async (userId) => { - const sessions = await prisma.authSession.findMany({ - where: { - user_id: userId - } - }); - return sessions.map((session) => transformDatabaseSession(session)); - }, - setUser: async (userId, attributes, key) => { - if (!key) { - return await prisma.authUser.create({ - data: { - id: userId, - ...attributes - } - }); - } - try { - const [databaseUser] = await prisma.$transaction([ - prisma.authUser.create({ - data: { - id: userId, - ...attributes - } - }), - prisma.authKey.create({ - data: key - }) - ] as const); - return databaseUser; - } catch (e) { - const error = e as Partial; - if (error.code === "P2002" && error.message?.includes("`id`")) - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - throw error; - } - }, - deleteUser: async (userId) => { - await prisma.authUser.deleteMany({ - where: { - id: userId - } - }); - }, - setSession: async (session) => { - try { - await prisma.authSession.create({ - data: session - }); - } catch (e) { - const error = e as Partial; - if (error.code === "P2003") - throw new LuciaError("AUTH_INVALID_USER_ID"); - if (error.code === "P2002" && error.message?.includes("`id`")) - throw new LuciaError("AUTH_DUPLICATE_SESSION_ID"); - throw error; - } - }, - deleteSession: async (sessionId) => { - await prisma.authSession.delete({ - where: { - id: sessionId - } - }); - }, - deleteSessionsByUserId: async (userId) => { - await prisma.authSession.deleteMany({ - where: { - user_id: userId - } - }); - }, - updateUserAttributes: async (userId, attributes) => { - try { - const databaseUser = await prisma.authUser.update({ - data: attributes, - where: { - id: userId - } - }); - return databaseUser; - } catch (e) { - const error = e as Partial; - if (error.code === "P2025") - throw new LuciaError("AUTH_INVALID_USER_ID"); - throw error; - } - }, - setKey: async (key) => { - try { - await prisma.authKey.create({ - data: key - }); - } catch (e) { - const error = e as Partial; - if (error.code === "P2003") - throw new LuciaError("AUTH_INVALID_USER_ID"); - if (error.code === "P2002" && error.message?.includes("`id`")) - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - throw error; - } - }, - getKey: async (keyId) => { - const databaseKey = await prisma.authKey.findUnique({ - where: { - id: keyId - } - }); - if (!databaseKey) return null; - return transformDatabaseKey(databaseKey); - }, - getKeysByUserId: async (userId) => { - const keys = await prisma.authKey.findMany({ - where: { - user_id: userId - } - }); - return keys.map((val) => transformDatabaseKey(val)); - }, - updateKeyPassword: async (keyId, hashedPassword) => { - try { - return await prisma.authKey.update({ - data: { - hashed_password: hashedPassword - }, - where: { - id: keyId - } - }); - } catch (e) { - const error = e as Partial; - if (error.code === "P2025") - throw new LuciaError("AUTH_INVALID_KEY_ID"); - throw error; - } - }, - deleteKeysByUserId: async (userId) => { - await prisma.authKey.deleteMany({ - where: { - user_id: userId - } - }); - }, - deleteNonPrimaryKey: async (keyId) => { - await prisma.authKey.deleteMany({ - where: { - id: keyId, - primary_key: false - } - }); - } - }; - }; - -export default adapter; +export { prismaAdapter } from "./prisma.js"; diff --git a/packages/adapter-prisma/src/lucia.d.ts b/packages/adapter-prisma/src/lucia.d.ts index f61de1891..8026ca988 100644 --- a/packages/adapter-prisma/src/lucia.d.ts +++ b/packages/adapter-prisma/src/lucia.d.ts @@ -1,5 +1,6 @@ -/// +/// declare namespace Lucia { type Auth = any; - type UserAttributes = {}; + type DatabaseUserAttributes = any; + type DatabaseSessionAttributes = any; } diff --git a/packages/adapter-prisma/src/prisma.ts b/packages/adapter-prisma/src/prisma.ts index c0cd0caf3..8c8952133 100644 --- a/packages/adapter-prisma/src/prisma.ts +++ b/packages/adapter-prisma/src/prisma.ts @@ -1,61 +1,256 @@ -type PayloadResult = { - count: number; -}; +import type { + Adapter, + InitializeAdapter, + KeySchema, + SessionSchema, + UserSchema +} from "lucia"; -type Model = { - schema: Record; - relations: Record>; +type PossiblePrismaError = { + code: string; + message: string; }; -export type PrismaClient> = { - [K in keyof Models]: { - findUnique: (options: { - where: Partial; - include?: any; - }) => Promise; - } & { [K: string]: any }; -} & { [K: string]: any }; +type ExtractModelNames<_PrismaClient extends PrismaClient> = Exclude< + keyof _PrismaClient, + `$${string}` +>; + +export const prismaAdapter = <_PrismaClient extends PrismaClient>(options: { + client: _PrismaClient; + models: { + user: ExtractModelNames<_PrismaClient>; + session: ExtractModelNames<_PrismaClient>; + key: ExtractModelNames<_PrismaClient>; + }; + tables?: { + user: string; + }; +}): InitializeAdapter => { + const User = options.client[ + options.models.user + ] as SmartPrismaModel; + const Session = options.client[ + options.models.session + ] as SmartPrismaModel; + const Key = options.client[options.models.key] as SmartPrismaModel; + + return (LuciaError) => { + return { + getUser: async (userId) => { + return await User.findUnique({ + where: { + id: userId + } + }); + }, + setUser: async (user, key) => { + if (!key) { + await User.create({ + data: user + }); + return; + } + try { + await options.client.$transaction([ + User.create({ + data: user + }), + Key.create({ + data: key + }) + ]); + } catch (e) { + const error = e as Partial; + if (error.code === "P2002" && error.message?.includes("`id`")) + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + throw error; + } + }, + deleteUser: async (userId) => { + await User.deleteMany({ + where: { + id: userId + } + }); + }, + updateUser: async (userId, partialUser) => { + await User.update({ + data: partialUser, + where: { + id: userId + } + }); + }, + getSession: async (sessionId) => { + const result = await Session.findUnique({ + where: { + id: sessionId + } + }); + if (!result) return null; + return transformPrismaSession(result); + }, + getSessionsByUserId: async (userId) => { + const sessions = await Session.findMany({ + where: { + user_id: userId + } + }); + return sessions.map((session) => transformPrismaSession(session)); + }, + setSession: async (session) => { + try { + await Session.create({ + data: session + }); + } catch (e) { + const error = e as Partial; + if (error.code === "P2003") { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + + if (error.code === "P2002" && error.message?.includes("`id`")) { + throw new LuciaError("AUTH_DUPLICATE_SESSION_ID"); + } + + throw error; + } + }, + deleteSession: async (sessionId) => { + await Session.delete({ + where: { + id: sessionId + } + }); + }, + deleteSessionsByUserId: async (userId) => { + await Session.deleteMany({ + where: { + user_id: userId + } + }); + }, + updateSession: async (userId, partialSession) => { + await Session.update({ + data: partialSession, + where: { + id: userId + } + }); + }, + + getKey: async (keyId) => { + return await Key.findUnique({ + where: { + id: keyId + } + }); + }, + getKeysByUserId: async (userId) => { + return await Key.findMany({ + where: { + user_id: userId + } + }); + }, + + setKey: async (key) => { + try { + await Key.create({ + data: key + }); + } catch (e) { + const error = e as Partial; + if (error.code === "P2003") { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + if (error.code === "P2002" && error.message?.includes("`id`")) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw error; + } + }, + deleteKey: async (keyId) => { + await Key.delete({ + where: { + id: keyId + } + }); + }, + deleteKeysByUserId: async (userId) => { + await Key.deleteMany({ + where: { + user_id: userId + } + }); + }, + updateKey: async (userId, partialKey) => { + await Key.update({ + data: partialKey, + where: { + id: userId + } + }); + }, -export type SmartPrismaClient> = { - [K in keyof Models]: { - findUnique: < - Options extends { - where: Partial; - include?: Partial>; + getSessionAndUser: async (sessionId) => { + const result = await Session.findUnique({ + where: { + id: sessionId + }, + include: { + [options.tables?.user ?? options.models.user]: true + } + }); + if (!result) return [null, null]; + const { + [options.tables?.user ?? options.models.user]: userResult, + ...sessionResult + } = result; + return [ + transformPrismaSession(sessionResult as PrismaSession), + userResult as UserSchema + ]; } - >( - options: Options - ) => Options["include"] extends undefined - ? Promise - : Promise< - | null - | (Models[K]["schema"] & { - [L in keyof Options["include"]]: L extends keyof Models[K]["relations"] - ? Models[K]["relations"][L] - : never; - }) - >; - findMany: (options: { - where: Partial; - }) => Promise; - create: (options: { - data: Models[K]["schema"]; - }) => Promise; - delete: (options: { - where: Partial; - }) => Promise; - deleteMany: (options: { - where: Partial; - }) => Promise; - update: (options: { - data: Partial; - where: Partial; - }) => Promise; + }; }; -} & { - $transaction: []>( - queries: Queries - ) => Promise<{ - [I in keyof Queries]: Awaited; - }>; +}; + +export const transformPrismaSession = ( + sessionData: PrismaSession +): SessionSchema => { + const { active_expires, idle_expires: idleExpires, ...data } = sessionData; + return { + ...data, + active_expires: Number(active_expires), + idle_expires: Number(idleExpires) + }; +}; + +type PrismaClient = { + $transaction: (...args: any) => any; +} & { [K: string]: any }; + +export type PrismaSession = Omit< + SessionSchema, + "active_expires" | "idle_expires" +> & { + active_expires: BigInt | number; + idle_expires: BigInt | number; +}; + +export type SmartPrismaModel<_Schema = any> = { + findUnique: <_Included = {}>(options: { + where: Partial<_Schema>; + include?: Partial>; + }) => Promise & _Included; + findMany: (options?: { where: Partial<_Schema> }) => Promise<_Schema[]>; + create: (options: { data: _Schema }) => Promise<_Schema>; + delete: (options: { where: Partial<_Schema> }) => Promise; + deleteMany: (options?: { where: Partial<_Schema> }) => Promise; + update: (options: { + data: Partial<_Schema>; + where: Partial<_Schema>; + }) => Promise<_Schema>; }; diff --git a/packages/adapter-prisma/src/utils.ts b/packages/adapter-prisma/src/utils.ts deleted file mode 100644 index 93af1504e..000000000 --- a/packages/adapter-prisma/src/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { KeySchema, SessionSchema } from "lucia-auth"; - -export const transformDatabaseSession = ( - sessionData: SessionSchema -): SessionSchema => { - const { active_expires, idle_expires: idleExpires, ...data } = sessionData; - return { - active_expires: Number(active_expires), - idle_expires: Number(idleExpires), - ...data - }; -}; - -export const transformDatabaseKey = (keyData: KeySchema): KeySchema => { - const { expires, ...data } = keyData; - return { - expires: expires === null ? null : Number(expires), - ...data - }; -}; diff --git a/packages/adapter-prisma/test/db.ts b/packages/adapter-prisma/test/db.ts deleted file mode 100644 index 067e27653..000000000 --- a/packages/adapter-prisma/test/db.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { PrismaClient } from "@prisma/client"; -import prisma from "../src/index.js"; -import { - transformDatabaseKey, - transformDatabaseSession -} from "../src/utils.js"; -import { KeySchema, LuciaError } from "lucia-auth"; -import type { LuciaQueryHandler } from "@lucia-auth/adapter-test"; - -const client = new PrismaClient(); -export const adapter = prisma(client)(LuciaError); - -export const db: LuciaQueryHandler = { - user: { - get: async () => { - return await client.authUser.findMany(); - }, - insert: async (user) => { - await client.authUser.create({ - data: user - }); - }, - clear: async () => { - await client.authUser.deleteMany(); - } - }, - session: { - get: async () => { - const sessions = await client.authSession.findMany(); - return sessions.map((session) => transformDatabaseSession(session)); - }, - insert: async (session) => { - await client.authSession.create({ - data: session - }); - }, - clear: async () => { - await client.authSession.deleteMany(); - } - }, - key: { - get: async () => { - const keys = (await client.authKey.findMany()) as unknown as KeySchema[]; - return keys.map((key) => transformDatabaseKey(key)); - }, - insert: async (key) => { - await client.authKey.create({ - data: key - }); - }, - clear: async () => { - await client.authKey.deleteMany(); - } - } -}; diff --git a/packages/adapter-prisma/test/index.ts b/packages/adapter-prisma/test/index.ts index febd175fb..0989c7bab 100644 --- a/packages/adapter-prisma/test/index.ts +++ b/packages/adapter-prisma/test/index.ts @@ -1,4 +1,56 @@ -import { testAdapter } from "@lucia-auth/adapter-test"; -import { db, adapter } from "./db.js"; +import { testAdapter, Database } from "@lucia-auth/adapter-test"; +import { LuciaError } from "lucia"; +import { PrismaClient } from "@prisma/client"; -testAdapter(adapter, db); +import { prismaAdapter, transformPrismaSession } from "../src/prisma.js"; + +import type { QueryHandler, TableQueryHandler } from "@lucia-auth/adapter-test"; +import type { SmartPrismaModel } from "../src/prisma.js"; + +const client = new PrismaClient(); + +const createTableQueryHandler = (model: any): TableQueryHandler => { + const Model = model as SmartPrismaModel; + return { + get: async () => { + return await Model.findMany(); + }, + insert: async (value: any) => { + await Model.create({ + data: value + }); + }, + clear: async () => { + await Model.deleteMany(); + } + }; +}; + +const queryHandler: QueryHandler = { + user: createTableQueryHandler(client.user), + session: { + ...createTableQueryHandler(client.session), + get: async () => { + const Session = client.session as any as SmartPrismaModel; + const result = await Session.findMany(); + return result.map((val) => transformPrismaSession(val)); + } + }, + key: createTableQueryHandler(client.key) +}; + +const adapter = prismaAdapter({ + client, + models: { + user: "user", + session: "session", + key: "key" + }, + tables: { + user: "test_user" + } +})(LuciaError); + +await testAdapter(adapter, new Database(queryHandler)); + +process.exit(0); diff --git a/packages/adapter-session-redis/.env.example b/packages/adapter-session-redis/.env.example new file mode 100644 index 000000000..634f46e4c --- /dev/null +++ b/packages/adapter-session-redis/.env.example @@ -0,0 +1 @@ +REDIS_PORT="" \ No newline at end of file diff --git a/packages/adapter-session-redis/.gitignore b/packages/adapter-session-redis/.gitignore index e30934148..7a0ae98eb 100644 --- a/packages/adapter-session-redis/.gitignore +++ b/packages/adapter-session-redis/.gitignore @@ -1,4 +1,5 @@ /node_modules /dist .DS_Store -.env \ No newline at end of file +.env +*.tgz \ No newline at end of file diff --git a/packages/adapter-session-redis/.npmignore b/packages/adapter-session-redis/.npmignore deleted file mode 100644 index b512c09d4..000000000 --- a/packages/adapter-session-redis/.npmignore +++ /dev/null @@ -1 +0,0 @@ -node_modules \ No newline at end of file diff --git a/packages/adapter-session-redis/package.json b/packages/adapter-session-redis/package.json index c763d1bc7..ab7d3bad8 100644 --- a/packages/adapter-session-redis/package.json +++ b/packages/adapter-session-redis/package.json @@ -2,21 +2,22 @@ "name": "@lucia-auth/adapter-session-redis", "version": "1.0.0", "description": "Redis session adapter for Lucia", - "main": "index.js", - "types": "index.d.ts", - "module": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "module": "dist/index.js", "type": "module", "files": [ - "**/*" + "/dist/", + "CHANGELOG.md" ], "scripts": { - "build": "shx rm -rf ./dist/* && tsc && shx cp ./package.json ./dist && shx cp ./README.md ./dist && shx cp .npmignore dist", + "build": "shx rm -rf ./dist/* && tsc", "test": "tsx test/index.ts", - "auri.publish": "pnpm build && cd dist && pnpm install --no-frozen-lockfile && pnpm publish --no-git-checks --access public && cd ../" + "auri.publish": "pnpm build && pnpm publish --no-git-checks --access public" }, "keywords": [ "lucia", - "lucia-auth", + "lucia", "auth", "authentication", "adapter", @@ -26,22 +27,22 @@ "repository": { "type": "git", "url": "https://github.com/pilcrowOnPaper/lucia", - "directory": "packages/session-adapter-redis" + "directory": "packages/adapter-session-redis" }, "author": "pilcrowonpaper", "license": "MIT", "exports": { - ".": "./index.js" + ".": "./dist/index.js" }, "peerDependencies": { - "lucia-auth": "1.x", - "redis": "4.x" + "lucia": "^2.0.0", + "redis": "^4.0.0" }, "devDependencies": { - "@lucia-auth/adapter-test": "workspace:*", + "@lucia-auth/adapter-test": "latest", "dotenv": "^16.0.3", "redis": "^4.3.1", "tsx": "^3.12.6", - "lucia-auth": "workspace:*" + "lucia": "latest" } } diff --git a/packages/adapter-session-redis/src/index.ts b/packages/adapter-session-redis/src/index.ts index 59c127277..23c8cabc4 100644 --- a/packages/adapter-session-redis/src/index.ts +++ b/packages/adapter-session-redis/src/index.ts @@ -1,65 +1 @@ -import type { - SessionSchema, - SessionAdapter, - AdapterFunction -} from "lucia-auth"; -import type { RedisClientType } from "redis"; - -const adapter = - (redisClient: { - session: RedisClientType; - userSession: RedisClientType; - }): AdapterFunction => - () => { - const { session: sessionRedis, userSession: userSessionRedis } = - redisClient; - return { - getSession: async (sessionId) => { - const sessionData = await sessionRedis.get(sessionId); - if (!sessionData) return null; - const session = JSON.parse(sessionData) as SessionSchema; - return session; - }, - getSessionsByUserId: async (userId) => { - const sessionIds = await userSessionRedis.lRange(userId, 0, -1); - const sessionData = await Promise.all( - sessionIds.map((id) => sessionRedis.get(id)) - ); - const sessions = sessionData - .filter((val): val is string => val !== null) - .map((val) => JSON.parse(val) as SessionSchema); - return sessions; - }, - setSession: async (session) => { - await Promise.all([ - userSessionRedis.lPush(session.user_id, session.id), - sessionRedis.set(session.id, JSON.stringify(session), { - EX: Math.floor(Number(session.idle_expires) / 1000) - }) - ]); - }, - deleteSession: async (...sessionIds) => { - const targetSessionData = await Promise.all( - sessionIds.map((id) => sessionRedis.get(id)) - ); - const sessions = targetSessionData - .filter((val): val is string => val !== null) - .map((val) => JSON.parse(val) as SessionSchema); - await Promise.all([ - ...sessionIds.map((id) => sessionRedis.del(id)), - ...sessions.map((session) => - userSessionRedis.lRem(session.user_id, 1, session.id) - ) - ]); - }, - deleteSessionsByUserId: async (userId) => { - const sessionIds = await userSessionRedis.lRange(userId, 0, -1); - await Promise.all([ - ...sessionIds.map((id) => sessionRedis.del(id)), - userSessionRedis.del(userId) - ]); - } - }; - }; - -export default adapter; +export { redisSessionAdapter as redis } from "./redis.js"; diff --git a/packages/adapter-session-redis/src/lucia.d.ts b/packages/adapter-session-redis/src/lucia.d.ts index 48a75f8ab..8026ca988 100644 --- a/packages/adapter-session-redis/src/lucia.d.ts +++ b/packages/adapter-session-redis/src/lucia.d.ts @@ -1,9 +1,6 @@ -/// +/// declare namespace Lucia { type Auth = any; - type UserAttributes = {}; -} - -declare namespace App { - interface Locals {} + type DatabaseUserAttributes = any; + type DatabaseSessionAttributes = any; } diff --git a/packages/adapter-session-redis/src/redis.ts b/packages/adapter-session-redis/src/redis.ts new file mode 100644 index 000000000..0344600f7 --- /dev/null +++ b/packages/adapter-session-redis/src/redis.ts @@ -0,0 +1,84 @@ +import type { SessionSchema, SessionAdapter, InitializeAdapter } from "lucia"; +import type { RedisClientType } from "redis"; + +export const DEFAULT_SESSION_PREFIX = "session"; +export const DEFAULT_USER_SESSIONS_PREFIX = "user_sessions"; + +export const redisSessionAdapter = ( + client: RedisClientType, + 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 client.get(sessionKey(sessionId)); + if (!sessionData) return null; + const session = JSON.parse(sessionData) as SessionSchema; + return session; + }, + getSessionsByUserId: async (userId) => { + const sessionIds = await client.sMembers(userSessionsKey(userId)); + const sessionData = await Promise.all( + sessionIds.map((sessionId) => client.get(sessionKey(sessionId))) + ); + const sessions = sessionData + .filter((val): val is string => val !== null) + .map((val) => JSON.parse(val) as SessionSchema); + return sessions; + }, + setSession: async (session) => { + await Promise.all([ + client.sAdd(userSessionsKey(session.user_id), session.id), + client.set(sessionKey(session.id), JSON.stringify(session), { + EX: Math.floor(Number(session.idle_expires) / 1000) + }) + ]); + }, + deleteSession: async (sessionId) => { + const sessionData = await client.get(sessionKey(sessionId)); + if (!sessionData) return; + const session = JSON.parse(sessionData) as SessionSchema; + await Promise.all([ + client.del(sessionKey(sessionId)), + client.sRem(userSessionsKey(session.user_id), sessionId) + ]); + }, + deleteSessionsByUserId: async (userId) => { + const sessionIds = await client.sMembers(userSessionsKey(userId)); + await Promise.all([ + ...sessionIds.map((sessionId) => client.del(sessionKey(sessionId))), + client.del(userSessionsKey(userId)) + ]); + }, + updateSession: async (sessionId, partialSession) => { + const sessionData = await client.get(sessionKey(sessionId)); + if (!sessionData) return; + const session = JSON.parse(sessionData) as SessionSchema; + const updatedSession = { + ...session, + ...partialSession + }; + await client.set( + sessionKey(sessionId), + JSON.stringify(updatedSession), + { + EX: Math.floor(Number(updatedSession.idle_expires) / 1000) + } + ); + } + }; + }; +}; diff --git a/packages/adapter-session-redis/test/db.ts b/packages/adapter-session-redis/test/db.ts deleted file mode 100644 index 30d099410..000000000 --- a/packages/adapter-session-redis/test/db.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { LuciaQueryHandler } from "@lucia-auth/adapter-test"; -import type { SessionSchema } from "lucia-auth/types.js"; - -import { createClient } from "redis"; -import redis from "../src/index.js"; -import { LuciaError } from "lucia-auth"; - -const sessionInstance = createClient({ - socket: { - port: 6379 - } -}); - -const userSessionInstance = createClient({ - socket: { - port: 6380 - } -}); - -await sessionInstance.connect(); -await userSessionInstance.connect(); - -export const adapter = redis({ - session: sessionInstance, - userSession: userSessionInstance -})(LuciaError); - -export const queryHandler: LuciaQueryHandler = { - session: { - get: async () => { - const sessionIds = await sessionInstance.keys("*"); - const sessionData = await Promise.all( - sessionIds.map((id) => sessionInstance.get(id)) - ); - const sessions = sessionData - .filter((val): val is string => val !== null) - .map((data) => JSON.parse(data) as SessionSchema); - - return sessions; - }, - insert: async (session) => { - await Promise.all([ - sessionInstance.set(session.id, JSON.stringify(session)), - userSessionInstance.lPush(session.user_id, session.id) - ]); - }, - clear: async () => { - await Promise.all([ - sessionInstance.flushAll(), - userSessionInstance.flushAll() - ]); - } - } -}; diff --git a/packages/adapter-session-redis/test/index.ts b/packages/adapter-session-redis/test/index.ts index 85c39b9e5..cbd55491d 100644 --- a/packages/adapter-session-redis/test/index.ts +++ b/packages/adapter-session-redis/test/index.ts @@ -1,4 +1,63 @@ -import { testSessionAdapter } from "@lucia-auth/adapter-test"; -import { queryHandler, adapter } from "./db.js"; +import { testSessionAdapter, Database } from "@lucia-auth/adapter-test"; +import { LuciaError } from "lucia"; +import dotenv from "dotenv"; +import { resolve } from "path"; -testSessionAdapter(adapter, queryHandler); +import { createClient } from "redis"; +import { + redisSessionAdapter, + DEFAULT_SESSION_PREFIX, + DEFAULT_USER_SESSIONS_PREFIX +} from "../src/redis.js"; + +import type { QueryHandler } from "@lucia-auth/adapter-test"; +import type { SessionSchema } from "lucia"; + +dotenv.config({ + path: `${resolve()}/.env` +}); + +const redisClient = createClient({ + socket: { + port: Number(process.env.REDIS_PORT) + } +}); + +const sessionKey = (sessionId: string) => { + return [DEFAULT_SESSION_PREFIX, sessionId].join(":"); +}; +const userSessionsKey = (userId: string) => { + return [DEFAULT_USER_SESSIONS_PREFIX, userId].join(":"); +}; + +const adapter = redisSessionAdapter(redisClient)(LuciaError); + +const queryHandler: QueryHandler = { + session: { + get: async () => { + const keys = await redisClient.keys(sessionKey("*")); + const sessionData = await Promise.all( + keys.map((key) => redisClient.get(key)) + ); + const sessions = sessionData + .filter((val): val is string => val !== null) + .map((data) => JSON.parse(data) as SessionSchema); + return sessions; + }, + insert: async (session) => { + await Promise.all([ + redisClient.set(sessionKey(session.id), JSON.stringify(session)), + redisClient.sAdd(userSessionsKey(session.user_id), session.id) + ]); + }, + clear: async () => { + await redisClient.flushAll(); + } + } +}; + +await redisClient.connect(); + +await testSessionAdapter(adapter, new Database(queryHandler)); + +process.exit(0); diff --git a/packages/adapter-sqlite/.gitignore b/packages/adapter-sqlite/.gitignore index ad2955eb0..9b5a48152 100644 --- a/packages/adapter-sqlite/.gitignore +++ b/packages/adapter-sqlite/.gitignore @@ -1,8 +1,6 @@ /node_modules /dist .DS_Store -/prisma/migrations -/prisma/dev.db -/prisma/dev.db-journal .env -/test/d1/wrangler.toml +*.tgz +*.tgz \ No newline at end of file diff --git a/packages/adapter-sqlite/.npmignore b/packages/adapter-sqlite/.npmignore deleted file mode 100644 index e1ac86668..000000000 --- a/packages/adapter-sqlite/.npmignore +++ /dev/null @@ -1,8 +0,0 @@ -/node_modules -.DS_Store -/src -/tsconfig.json -.gitignore -.env -/prisma -/test \ No newline at end of file diff --git a/packages/adapter-sqlite/.wrangler/state/d1/DB.sqlite3 b/packages/adapter-sqlite/.wrangler/state/d1/DB.sqlite3 deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/adapter-sqlite/package.json b/packages/adapter-sqlite/package.json index 205878937..e6dec229e 100644 --- a/packages/adapter-sqlite/package.json +++ b/packages/adapter-sqlite/package.json @@ -2,22 +2,23 @@ "name": "@lucia-auth/adapter-sqlite", "version": "1.1.1", "description": "SQLite adapter for Lucia", - "main": "index.js", - "types": "index.d.ts", - "module": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "module": "dist/index.js", "type": "module", "files": [ - "**/*" + "/dist/", + "CHANGELOG.md" ], "scripts": { - "build": "shx rm -rf ./dist/* && tsc && shx cp ./package.json ./dist && shx cp ./README.md ./dist && shx cp .npmignore dist", + "build": "shx rm -rf ./dist/* && tsc", + "auri.publish": "pnpm build && pnpm publish --no-git-checks --access public", "test.better-sqlite3": "tsx test/better-sqlite3/index.ts", - "test.d1": "tsx test/d1/generate.ts && wrangler dev test/d1/index.ts --local --persist", - "auri.publish": "pnpm build && cd dist && pnpm install --no-frozen-lockfile && pnpm publish --no-git-checks --access public && cd ../" + "test.d1": "tsx test/d1/index.ts" }, "keywords": [ "lucia", - "lucia-auth", + "lucia", "auth", "better-sqlite3", "sqlite", @@ -30,16 +31,16 @@ "repository": { "type": "git", "url": "https://github.com/pilcrowOnPaper/lucia", - "directory": "packages/adapter-prisma" + "directory": "packages/adapter-sqlite" }, "author": "pilcrowonpaper", "license": "MIT", "exports": { - ".": "./index.js" + ".": "./dist/index.js" }, "peerDependencies": { "better-sqlite3": "^8.0.0", - "lucia-auth": "^1.4.0" + "lucia": "^2.0.0" }, "peerDependenciesMeta": { "better-sqlite3": { @@ -47,12 +48,12 @@ } }, "devDependencies": { - "@cloudflare/workers-types": "^4.20230404.0", - "@lucia-auth/adapter-test": "workspace:*", + "@cloudflare/workers-types": "^4.20230518.0", + "@lucia-auth/adapter-test": "latest", + "@miniflare/d1": "^2.14.0", "@types/better-sqlite3": "^7.6.3", - "better-sqlite3": "^8.0.1", - "dotenv": "^16.0.3", - "lucia-auth": "workspace:*", + "better-sqlite3": "^8.4.0", + "lucia": "latest", "tsx": "^3.12.6" } } diff --git a/packages/adapter-sqlite/src/better-sqlite3/index.ts b/packages/adapter-sqlite/src/better-sqlite3/index.ts deleted file mode 100644 index 6b468f523..000000000 --- a/packages/adapter-sqlite/src/better-sqlite3/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { createOperator } from "../query.js"; -import { betterSqliteRunner } from "./runner.js"; -import { transformToSqliteValue } from "../utils.js"; -import { createCoreAdapter } from "../core.js"; - -import type { Adapter, AdapterFunction } from "lucia-auth"; -import type { Database, SqliteError } from "better-sqlite3"; -import type { SQLiteUserSchema } from "../utils.js"; - -type BetterSQLiteError = SqliteError["prototype"]; - -export const betterSqlite3 = (db: Database): AdapterFunction => { - const transaction = async <_Execute extends () => Promise>( - execute: _Execute - ): Promise>> => { - try { - db.exec("BEGIN TRANSACTION"); - const result = execute(); - db.exec("COMMIT"); - return result; - } catch (e) { - if (db.inTransaction) { - db.exec("ROLLBACK"); - } - throw e; - } - }; - - return (LuciaError) => { - const operator = createOperator(betterSqliteRunner(db)); - const coreAdapter = createCoreAdapter(operator); - return { - getUser: coreAdapter.getUser, - getSessionAndUserBySessionId: coreAdapter.getSessionAndUserBySessionId, - getSession: coreAdapter.getSession, - getSessionsByUserId: coreAdapter.getSessionsByUserId, - setUser: async (userId, attributes, key) => { - const user = { - id: userId, - ...attributes - }; - try { - if (key) { - const databaseUser = await transaction(async () => { - const databaseUser = await operator.get( - (ctx) => [ctx.insertInto("auth_user", user), ctx.returning("*")] - ); - if (!databaseUser) throw new TypeError("Unexpected query result"); - await operator.run((ctx) => [ - ctx.insertInto("auth_key", transformToSqliteValue(key)) - ]); - return databaseUser; - }); - return databaseUser; - } - const databaseUser = await operator.get((ctx) => [ - ctx.insertInto("auth_user", user), - ctx.returning("*") - ]); - if (!databaseUser) throw new TypeError("Unexpected type"); - return databaseUser; - } catch (e) { - const error = e as Partial; - if ( - error.code === "SQLITE_CONSTRAINT_PRIMARYKEY" && - error.message?.includes(".id") - ) { - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - throw e; - } - }, - deleteUser: coreAdapter.deleteUser, - setSession: async (session) => { - try { - return await coreAdapter.setSession(session); - } catch (e) { - const error = e as Partial; - if (error.code === "SQLITE_CONSTRAINT_FOREIGNKEY") { - throw new LuciaError("AUTH_INVALID_USER_ID"); - } - if ( - error.code === "SQLITE_CONSTRAINT_PRIMARYKEY" && - error.message?.includes(".id") - ) { - throw new LuciaError("AUTH_DUPLICATE_SESSION_ID"); - } - throw e; - } - }, - deleteSession: coreAdapter.deleteSession, - deleteSessionsByUserId: coreAdapter.deleteSessionsByUserId, - updateUserAttributes: coreAdapter.updateUserAttributes, - setKey: async (key) => { - try { - return await coreAdapter.setKey(key); - } catch (e) { - const error = e as Partial; - if (error.code === "SQLITE_CONSTRAINT_FOREIGNKEY") { - throw new LuciaError("AUTH_INVALID_USER_ID"); - } - if ( - error.code === "SQLITE_CONSTRAINT_PRIMARYKEY" && - error.message?.includes(".id") - ) { - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - throw e; - } - }, - getKey: coreAdapter.getKey, - getKeysByUserId: coreAdapter.getKeysByUserId, - updateKeyPassword: coreAdapter.updateKeyPassword, - deleteKeysByUserId: coreAdapter.deleteKeysByUserId, - deleteNonPrimaryKey: coreAdapter.deleteNonPrimaryKey - }; - }; -}; diff --git a/packages/adapter-sqlite/src/better-sqlite3/runner.ts b/packages/adapter-sqlite/src/better-sqlite3/runner.ts deleted file mode 100644 index aff9a1cd3..000000000 --- a/packages/adapter-sqlite/src/better-sqlite3/runner.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Runner } from "../query.js"; -import type { Database } from "better-sqlite3"; - -export const betterSqliteRunner = (db: Database): Runner => { - return { - get: async (query, params) => { - return db.prepare(query).all(params); - }, - run: async (query, params) => { - db.prepare(query).run(params); - } - }; -}; diff --git a/packages/adapter-sqlite/src/core.ts b/packages/adapter-sqlite/src/core.ts deleted file mode 100644 index 56a1fae04..000000000 --- a/packages/adapter-sqlite/src/core.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { - transformDatabaseSession, - transformDatabaseKey, - transformToSqliteValue -} from "./utils.js"; - -import type { - SQLiteKeySchema, - SQLiteSessionSchema, - SQLiteUserSchema -} from "./utils.js"; -import type { Adapter } from "lucia-auth"; -import type { Operator } from "./query.js"; - -export const createCoreAdapter = (operator: Operator) => { - return { - getUser: async (userId) => { - return operator.get((ctx) => [ - ctx.selectFrom("auth_user", "*"), - ctx.where("id", "=", userId) - ]); - }, - getSessionAndUserBySessionId: async (sessionId) => { - const data = await operator.get< - SQLiteUserSchema & { - _session_active_expires: number; - _session_id: string; - _session_idle_expires: number; - _session_user_id: string; - } - >((ctx) => [ - ctx.selectFrom( - "auth_session", - "auth_user.*", - "auth_session.id as _session_id", - "auth_session.active_expires as _session_active_expires", - "auth_session.idle_expires as _session_idle_expires", - "auth_session.user_id as _session_user_id" - ), - ctx.innerJoin("auth_user", "auth_user.id", "auth_session.user_id"), - ctx.where("auth_session.id", "=", sessionId) - ]); - if (!data) return null; - const { - _session_active_expires, - _session_id, - _session_idle_expires, - _session_user_id, - ...user - } = data; - return { - user, - session: transformDatabaseSession({ - id: _session_id, - user_id: _session_user_id, - active_expires: _session_active_expires, - idle_expires: _session_idle_expires - }) - }; - }, - getSession: async (sessionId) => { - const databaseSession = await operator.get((ctx) => [ - ctx.selectFrom("auth_session", "*"), - ctx.where("id", "=", sessionId) - ]); - if (!databaseSession) return null; - return transformDatabaseSession(databaseSession); - }, - getSessionsByUserId: async (userId) => { - const databaseSessions = await operator.getAll( - (ctx) => [ - ctx.selectFrom("auth_session", "*"), - ctx.where("user_id", "=", userId) - ] - ); - return databaseSessions.map((val) => transformDatabaseSession(val)); - }, - deleteUser: async (userId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_user"), - ctx.where("id", "=", userId) - ]); - }, - setSession: async (session) => { - await operator.run((ctx) => [ctx.insertInto("auth_session", session)]); - }, - deleteSession: async (sessionId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_session"), - ctx.where("id", "=", sessionId) - ]); - }, - deleteSessionsByUserId: async (userId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_session"), - ctx.where("user_id", "=", userId) - ]); - }, - updateUserAttributes: async (userId, attributes) => { - if (Object.keys(attributes).length === 0) { - operator.run((ctx) => [ - ctx.selectFrom("auth_user", "*"), - ctx.where("id", "=", userId) - ]); - return; - } - await operator.run((ctx) => [ - ctx.update("auth_user", attributes), - ctx.where("id", "=", userId) - ]); - }, - setKey: async (key) => { - await operator.run((ctx) => [ - ctx.insertInto("auth_key", transformToSqliteValue(key)) - ]); - }, - getKey: async (keyId) => { - const databaseKey = await operator.get((ctx) => [ - ctx.selectFrom("auth_key", "*"), - ctx.where("id", "=", keyId) - ]); - if (!databaseKey) return null; - const transformedDatabaseKey = transformDatabaseKey(databaseKey); - return transformedDatabaseKey; - }, - getKeysByUserId: async (userId) => { - const databaseKeys = await operator.getAll((ctx) => [ - ctx.selectFrom("auth_key", "*"), - ctx.where("user_id", "=", userId) - ]); - return databaseKeys.map((val) => transformDatabaseKey(val)); - }, - updateKeyPassword: async (key, hashedPassword) => { - await operator.run((ctx) => [ - ctx.update("auth_key", { - hashed_password: hashedPassword - }), - ctx.where("id", "=", key) - ]); - }, - deleteKeysByUserId: async (userId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_key"), - ctx.where("user_id", "=", userId) - ]); - }, - deleteNonPrimaryKey: async (keyId) => { - await operator.run((ctx) => [ - ctx.deleteFrom("auth_key"), - ctx.and( - ctx.where("id", "=", keyId), - ctx.where("primary_key", "=", Number(false)) - ) - ]); - } - } satisfies Partial; -}; diff --git a/packages/adapter-sqlite/src/d1/index.ts b/packages/adapter-sqlite/src/d1/index.ts deleted file mode 100644 index 8060c44b0..000000000 --- a/packages/adapter-sqlite/src/d1/index.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { createOperator } from "../query.js"; -import { d1Runner } from "./runner.js"; -import { transformToSqliteValue } from "../utils.js"; -import { createCoreAdapter } from "../core.js"; - -import type { Adapter, AdapterFunction, UserSchema } from "lucia-auth"; -import type { SQLiteUserSchema } from "../utils.js"; -import type { D1Database } from "@cloudflare/workers-types"; - -export const d1 = (db: D1Database): AdapterFunction => { - return (LuciaError) => { - const operator = createOperator(d1Runner(db)); - const coreAdapter = createCoreAdapter(operator); - return { - ...coreAdapter, - setUser: async (userId, attributes, key) => { - const user = { - id: userId, - ...attributes - }; - try { - if (key) { - const setUserQuery = operator.write((ctx) => [ - ctx.insertInto("auth_user", user), - ctx.returning("*") - ]); - const setKeyQuery = operator.write((ctx) => [ - ctx.insertInto("auth_key", transformToSqliteValue(key)) - ]); - const [setUserResult] = await db.batch([ - db.prepare(setUserQuery.statement).bind(...setUserQuery.params), - db.prepare(setKeyQuery.statement).bind(...setKeyQuery.params) - ]); - if (setUserResult.error) throw setUserResult.error; - if (!setUserResult.results || setUserResult.results.length < 1) - throw new Error("Unexpected value"); - return setUserResult.results[0] as UserSchema; - } - const databaseUser = await operator.get((ctx) => [ - ctx.insertInto("auth_user", user), - ctx.returning("*") - ]); - if (!databaseUser) throw new TypeError("Unexpected type"); - return databaseUser; - } catch (e) { - const error = e as Partial<{ - cause: Partial; - }>; - if ( - error.cause?.message?.includes("UNIQUE constraint failed") && - error.cause?.message?.includes("auth_key.id") - ) { - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - throw e; - } - }, - setSession: async (session) => { - try { - return await coreAdapter.setSession(session); - } catch (e) { - const error = e as Partial<{ - cause: Partial; - }>; - if (error.cause?.message?.includes("FOREIGN KEY constraint failed")) { - throw new LuciaError("AUTH_INVALID_USER_ID"); - } - if ( - error.cause?.message?.includes("UNIQUE constraint failed") && - error.cause?.message?.includes("auth_session.id") - ) { - throw new LuciaError("AUTH_DUPLICATE_SESSION_ID"); - } - throw e; - } - }, - setKey: async (key) => { - try { - return await coreAdapter.setKey(key); - } catch (e) { - const error = e as Partial<{ - cause: Partial; - }>; - if (error.cause?.message?.includes("FOREIGN KEY constraint failed")) { - throw new LuciaError("AUTH_INVALID_USER_ID"); - } - if ( - error.cause?.message?.includes("UNIQUE constraint failed") && - error.cause?.message?.includes("auth_key.id") - ) { - throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); - } - throw e; - } - } - }; - }; -}; diff --git a/packages/adapter-sqlite/src/d1/runner.ts b/packages/adapter-sqlite/src/d1/runner.ts deleted file mode 100644 index 65327b772..000000000 --- a/packages/adapter-sqlite/src/d1/runner.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Runner } from "../query.js"; -import type { D1Database } from "@cloudflare/workers-types"; - -export const d1Runner = (db: D1Database): Runner => { - return { - get: async (query, params) => { - const result = await db - .prepare(query) - .bind(...params) - .all(); - if (result.error) throw result.error; - return result.results ?? []; - }, - run: async (query, params) => { - const result = await db - .prepare(query) - .bind(...params) - .run(); - if (result.error) throw result.error; - } - }; -}; diff --git a/packages/adapter-sqlite/src/drivers/better-sqlite3.ts b/packages/adapter-sqlite/src/drivers/better-sqlite3.ts new file mode 100644 index 000000000..361c71cdb --- /dev/null +++ b/packages/adapter-sqlite/src/drivers/better-sqlite3.ts @@ -0,0 +1,191 @@ +import { helper, getSetArgs, escapeName } from "../utils.js"; + +import type { + SessionSchema, + Adapter, + InitializeAdapter, + UserSchema, + KeySchema +} from "lucia"; +import type { Database, SqliteError } from "better-sqlite3"; + +type BetterSQLiteError = InstanceType; + +export const betterSqlite3Adapter = ( + db: Database, + tables: { + user: string; + session: string; + key: string; + } +): InitializeAdapter => { + const transaction = <_Query extends () => any>(query: _Query): void => { + try { + db.exec("BEGIN TRANSACTION"); + const result = query(); + db.exec("COMMIT"); + return result; + } catch (e) { + if (db.inTransaction) { + db.exec("ROLLBACK"); + } + throw e; + } + }; + + const ESCAPED_USER_TABLE_NAME = escapeName(tables.user); + const ESCAPED_SESSION_TABLE_NAME = escapeName(tables.session); + const ESCAPED_KEY_TABLE_NAME = escapeName(tables.key); + + return (LuciaError) => { + return { + getUser: async (userId) => { + const result: UserSchema | undefined = db + .prepare(`SELECT * FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = ?`) + .get(userId); + return result ?? null; + }, + setUser: async (user, key) => { + const insertUser = () => { + const [userFields, userValues, userArgs] = helper(user); + db.prepare( + `INSERT INTO ${ESCAPED_USER_TABLE_NAME} ( ${userFields} ) VALUES ( ${userValues} )` + ).run(...userArgs); + }; + if (!key) return insertUser(); + try { + transaction(() => { + insertUser(); + const [keyFields, keyValues, keyArgs] = helper(key); + db.prepare( + `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${keyFields} ) VALUES ( ${keyValues} )` + ).run(...keyArgs); + }); + } catch (e) { + const error = e as Partial; + if ( + error.code === "SQLITE_CONSTRAINT_PRIMARYKEY" && + error.message?.includes(".id") + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw e; + } + }, + deleteUser: async (userId) => { + db.prepare(`DELETE FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = ?`).run( + userId + ); + }, + updateUser: async (userId, partialUser) => { + const [fields, values, args] = helper(partialUser); + db.prepare( + `UPDATE ${ESCAPED_USER_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?` + ).run(...args, userId); + }, + + getSession: async (sessionId) => { + const result: SessionSchema | undefined = db + .prepare(`SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`) + .get(sessionId); + return result ?? null; + }, + getSessionsByUserId: async (userId) => { + const result: SessionSchema[] = db + .prepare( + `SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = ?` + ) + .all(userId); + return result; + }, + setSession: async (session) => { + try { + const [fields, values, args] = helper(session); + db.prepare( + `INSERT INTO ${ESCAPED_SESSION_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )` + ).run(...args); + } catch (e) { + const error = e as Partial; + if (error.code === "SQLITE_CONSTRAINT_FOREIGNKEY") { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + throw e; + } + }, + deleteSession: async (sessionId) => { + db.prepare( + `DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?` + ).run(sessionId); + }, + deleteSessionsByUserId: async (userId) => { + db.prepare( + `DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = ?` + ).run(userId); + }, + updateSession: async (sessionId, partialSession) => { + const [fields, values, args] = helper(partialSession); + db.prepare( + `UPDATE ${ESCAPED_SESSION_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?` + ).run(...args, sessionId); + }, + + getKey: async (keyId) => { + const result: KeySchema | undefined = db + .prepare(`SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = ?`) + .get(keyId); + return result ?? null; + }, + getKeysByUserId: async (userId) => { + const result: KeySchema[] = db + .prepare(`SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = ?`) + .all(userId); + return result; + }, + setKey: async (key) => { + try { + const [fields, values, args] = helper(key); + db.prepare( + `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )` + ).run(...args); + } catch (e) { + const error = e as Partial; + if (error.code === "SQLITE_CONSTRAINT_FOREIGNKEY") { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + if ( + error.code === "SQLITE_CONSTRAINT_PRIMARYKEY" && + error.message?.includes(".id") + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw e; + } + }, + deleteKey: async (keyId) => { + db.prepare(`DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = ?`).run( + keyId + ); + }, + deleteKeysByUserId: async (userId) => { + db.prepare( + `DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = ?` + ).run(userId); + }, + updateKey: async (keyId, partialKey) => { + const [fields, values, args] = helper(partialKey); + db.prepare( + `UPDATE ${ESCAPED_KEY_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?` + ).run(...args, keyId); + } + }; + }; +}; diff --git a/packages/adapter-sqlite/src/drivers/d1.ts b/packages/adapter-sqlite/src/drivers/d1.ts new file mode 100644 index 000000000..c3f96c5d2 --- /dev/null +++ b/packages/adapter-sqlite/src/drivers/d1.ts @@ -0,0 +1,243 @@ +import { helper, getSetArgs, escapeName } from "../utils.js"; + +import type { + SessionSchema, + Adapter, + InitializeAdapter, + UserSchema, + KeySchema +} from "lucia"; +import type { D1Database } from "@cloudflare/workers-types"; + +export const d1Adapter = ( + db: D1Database, + tables: { + user: string; + session: string; + key: string; + } +): InitializeAdapter => { + const ESCAPED_USER_TABLE_NAME = escapeName(tables.user); + const ESCAPED_SESSION_TABLE_NAME = escapeName(tables.session); + const ESCAPED_KEY_TABLE_NAME = escapeName(tables.key); + + return (LuciaError) => { + return { + getUser: async (userId) => { + const user = await db + .prepare(`SELECT * FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = ?`) + .bind(userId) + .first(); + return user; + }, + setUser: async (user, key) => { + const [userFields, userValues, userArgs] = helper(user); + const insertUserStatement = db + .prepare( + `INSERT INTO ${ESCAPED_USER_TABLE_NAME} ( ${userFields} ) VALUES ( ${userValues} )` + ) + .bind(...userArgs); + if (!key) { + await insertUserStatement.run(); + return; + } + try { + const [keyFields, keyValues, keyArgs] = helper(key); + const insertKeyStatement = db + .prepare( + `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${keyFields} ) VALUES ( ${keyValues} )` + ) + .bind(...keyArgs); + await db.batch([insertUserStatement, insertKeyStatement]); + } catch (e) { + const error = e as Partial<{ + cause: Partial; + }>; + if ( + error.cause?.message?.includes("UNIQUE constraint failed") && + error.cause?.message?.includes(`${tables.key}.id`) + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw e; + } + }, + deleteUser: async (userId) => { + await db + .prepare(`DELETE FROM ${ESCAPED_USER_TABLE_NAME} WHERE id = ?`) + .bind(userId) + .run(); + }, + updateUser: async (userId, partialUser) => { + const [fields, values, args] = helper(partialUser); + await db + .prepare( + `UPDATE ${ESCAPED_USER_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?` + ) + .bind(...args, userId) + .run(); + }, + + getSession: async (sessionId) => { + const session = await db + .prepare(`SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`) + .bind(sessionId) + .first(); + return session; + }, + getSessionsByUserId: async (userId) => { + const { results: sessionResults } = await db + .prepare( + `SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = ?` + ) + .bind(userId) + .all(); + return sessionResults ?? []; + }, + setSession: async (session) => { + try { + const [fields, values, args] = helper(session); + await db + .prepare( + `INSERT INTO ${ESCAPED_SESSION_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )` + ) + .bind(...args) + .run(); + } catch (e) { + const error = e as Partial<{ + cause: Partial; + }>; + if (error.cause?.message?.includes("FOREIGN KEY constraint failed")) { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + throw e; + } + }, + deleteSession: async (sessionId) => { + await db + .prepare(`DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`) + .bind(sessionId) + .run(); + }, + deleteSessionsByUserId: async (userId) => { + await db + .prepare( + `DELETE FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE user_id = ?` + ) + .bind(userId) + .run(); + }, + updateSession: async (sessionId, partialSession) => { + const [fields, values, args] = helper(partialSession); + await db + .prepare( + `UPDATE ${ESCAPED_SESSION_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?` + ) + .bind(...args, sessionId) + .run(); + }, + + getKey: async (keyId) => { + const key = await db + .prepare(`SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = ?`) + .bind(keyId) + .first(); + return key; + }, + getKeysByUserId: async (userId) => { + const { results: keyResults } = await db + .prepare(`SELECT * FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = ?`) + .bind(userId) + .all(); + return keyResults ?? []; + }, + setKey: async (key) => { + try { + const [fields, values, args] = helper(key); + await db + .prepare( + `INSERT INTO ${ESCAPED_KEY_TABLE_NAME} ( ${fields} ) VALUES ( ${values} )` + ) + .bind(...args) + .run(); + } catch (e) { + const error = e as Partial<{ + cause: Partial; + }>; + if (error.cause?.message?.includes("FOREIGN KEY constraint failed")) { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + if ( + error.cause?.message?.includes("UNIQUE constraint failed") && + error.cause?.message?.includes(`${tables.key}.id`) + ) { + throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); + } + throw e; + } + }, + deleteKey: async (keyId) => { + await db + .prepare(`DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE id = ?`) + .bind(keyId) + .run(); + }, + deleteKeysByUserId: async (userId) => { + await db + .prepare(`DELETE FROM ${ESCAPED_KEY_TABLE_NAME} WHERE user_id = ?`) + .bind(userId) + .run(); + }, + updateKey: async (keyId, partialKey) => { + const [fields, values, args] = helper(partialKey); + await db + .prepare( + `UPDATE ${ESCAPED_KEY_TABLE_NAME} SET ${getSetArgs( + fields, + values + )} WHERE id = ?` + ) + .bind(...args, keyId) + .run(); + }, + + getSessionAndUser: async (sessionId) => { + const getSessionStatement = db + .prepare(`SELECT * FROM ${ESCAPED_SESSION_TABLE_NAME} WHERE id = ?`) + .bind(sessionId); + const getUserFromJoinStatement = db + .prepare( + `SELECT ${ESCAPED_USER_TABLE_NAME}.*, ${ESCAPED_SESSION_TABLE_NAME}.id as __session_id FROM ${ESCAPED_SESSION_TABLE_NAME} INNER JOIN ${ESCAPED_USER_TABLE_NAME} ON ${ESCAPED_USER_TABLE_NAME}.id = ${ESCAPED_SESSION_TABLE_NAME}.user_id WHERE ${ESCAPED_SESSION_TABLE_NAME}.id = ?` + ) + .bind(sessionId); + type BatchQueryResult = { + error: any; + results?: Schema[]; + }; + const [{ results: sessionResults }, { results: userFromJoinResults }] = + (await db.batch([ + getSessionStatement, + getUserFromJoinStatement + ])) as any as [ + BatchQueryResult, + BatchQueryResult< + UserSchema & { + __session_id: string; + } + > + ]; + const sessionResult = sessionResults?.at(0) ?? null; + const userFromJoinResult = userFromJoinResults?.at(0) ?? null; + if (!sessionResult || !userFromJoinResult) return [null, null]; + const { __session_id: _, ...userResult } = userFromJoinResult; + return [sessionResult, userResult]; + } + }; + }; +}; diff --git a/packages/adapter-sqlite/src/index.ts b/packages/adapter-sqlite/src/index.ts index b36f2927d..21a65d4de 100644 --- a/packages/adapter-sqlite/src/index.ts +++ b/packages/adapter-sqlite/src/index.ts @@ -1,2 +1,2 @@ -export { betterSqlite3 } from "./better-sqlite3/index.js"; -export { d1 } from "./d1/index.js"; +export { betterSqlite3Adapter as betterSqlite3 } from "./drivers/better-sqlite3.js"; +export { d1Adapter as d1 } from "./drivers/d1.js"; diff --git a/packages/adapter-sqlite/src/lucia.d.ts b/packages/adapter-sqlite/src/lucia.d.ts index f61de1891..8026ca988 100644 --- a/packages/adapter-sqlite/src/lucia.d.ts +++ b/packages/adapter-sqlite/src/lucia.d.ts @@ -1,5 +1,6 @@ -/// +/// declare namespace Lucia { type Auth = any; - type UserAttributes = {}; + type DatabaseUserAttributes = any; + type DatabaseSessionAttributes = any; } diff --git a/packages/adapter-sqlite/src/query.ts b/packages/adapter-sqlite/src/query.ts deleted file mode 100644 index 4978dbfba..000000000 --- a/packages/adapter-sqlite/src/query.ts +++ /dev/null @@ -1,250 +0,0 @@ -const resolveQueryBlock = (block: Block): ResolvedBlock => { - const escapeName = (val: string) => { - if (val === "*") return val; - return `\`${val}\``; - }; - if (block.type === "DELETE_FROM") { - return { - queryChunk: `DELETE FROM ${escapeName(block.table)}`, - params: [] - }; - } - if (block.type === "INNER_JOIN") { - return { - queryChunk: `INNER JOIN ${escapeName(block.targetTable)} ON ${ - block.targetColumn - } = ${block.column}`, - params: [] - }; - } - if (block.type === "RETURNING") { - return { - queryChunk: `RETURNING ${block.columns}`, - params: [] - }; - } - if (block.type === "INSERT_INTO") { - const keys = Object.keys(block.values); - return { - queryChunk: `INSERT INTO ${escapeName(block.table)} (${keys.map((k) => - escapeName(k) - )}) VALUES (${Array(keys.length).fill("?")})`, - params: keys.map((k) => block.values[k]) - }; - } - if (block.type === "SELECT") { - return { - queryChunk: `SELECT ${block.columns} FROM ${escapeName(block.table)}`, - params: [] - }; - } - if (block.type === "WHERE") { - return { - queryChunk: `WHERE ${block.column} ${block.comparator} ?`, - params: [block.value] - }; - } - if (block.type === "UPDATE") { - const keys = Object.keys(block.values); - return { - queryChunk: `UPDATE ${escapeName(block.table)} SET ${keys.map((k) => { - return `${escapeName(k)} = ?`; - })}`, - params: keys.map((k) => block.values[k]) - }; - } - if (block.type === "AND") { - const resolvedConditionQueryBlocks = block.whereBlocks.map((whereBlock) => { - return { - queryChunk: `${whereBlock.column} ${whereBlock.comparator} ?`, - params: [whereBlock.value] - }; - }); - const conditionQueryChunk = resolvedConditionQueryBlocks - .map((resolvedBlock) => resolvedBlock.queryChunk) - .join(" AND "); - return { - queryChunk: `WHERE ${conditionQueryChunk}`, - params: resolvedConditionQueryBlocks.reduce( - (acc, curr) => [...acc, ...curr.params], - [] as ColumnValue[] - ) - }; - } - throw new TypeError(`Invalid block type`); -}; - -const ctx = { - innerJoin: (targetTable: string, targetColumn: string, column: string) => { - return { - type: "INNER_JOIN", - targetTable, - targetColumn, - column - }; - }, - returning: (...columns: [string, ...string[]]) => { - return { - type: "RETURNING", - columns - }; - }, - selectFrom: (table: string, ...columns: [string, ...string[]]) => { - return { - type: "SELECT", - table, - columns - }; - }, - insertInto: (table: string, values: Record) => { - return { - type: "INSERT_INTO", - table, - values - }; - }, - where: (column: string, comparator: string, value: ColumnValue) => { - return { - type: "WHERE", - column, - comparator, - value - }; - }, - deleteFrom: (table: string) => { - return { - type: "DELETE_FROM", - table - }; - }, - update: (table: string, values: Record) => { - return { - type: "UPDATE", - table, - values - }; - }, - and: (...whereBlocks: WhereBlock[]) => { - return { - type: "AND", - whereBlocks - }; - } -} satisfies Record Block>; - -export const createOperator = <_Runner extends Runner>(runner: _Runner) => { - const resolveQueryBlocks = (blocks: Block[]) => { - const resolvedBlocks = blocks.map(resolveQueryBlock); - const statement = resolvedBlocks.map((block) => block.queryChunk).join(" "); - const params = resolvedBlocks.reduce((result, block) => { - result.push(...block.params); - return result; - }, [] as ColumnValue[]); - return { - statement, - params - }; - }; - - const write = <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ) => { - const blocks = createQueryBlocks(ctx); - return resolveQueryBlocks(blocks); - }; - const get = async <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ): Promise<_Selection | null> => { - const query = write(createQueryBlocks); - const result = await runner.get(query.statement, query.params); - if (Array.isArray(result)) return result.at(0) ?? null; - return result ?? null; - }; - const getAll = async <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ): Promise<_Selection[]> => { - const query = write(createQueryBlocks); - const result = await runner.get(query.statement, query.params); - if (!result) return [] as any; - if (!Array.isArray(result)) return [result] as any; - return result as any; - }; - const run = <_Selection extends Record>( - createQueryBlocks: CreateQueryBlocks - ): Promise => { - const query = write(createQueryBlocks); - return runner.run(query.statement, query.params) as any; - }; - return { - write, - get, - getAll, - run - } as const; -}; - -export type Operator = ReturnType; - -export type Context = typeof ctx; - -type CreateQueryBlocks = (context: Context) => Block[]; - -type ResolvedBlock = { - queryChunk: string; - params: ColumnValue[]; -}; - -export type Runner = { - get: (statement: string, params: ColumnValue[]) => Promise; - run: (statement: string, params: ColumnValue[]) => Promise; -}; -type ColumnValue = string | number | null | bigint; - -type Block = - | { - type: "INNER_JOIN"; - targetTable: string; - targetColumn: string; - column: string; - } - | { - type: "SELECT"; - table: string; - columns: string[]; - } - | { - type: "INSERT_INTO"; - table: string; - values: Record; - } - | { - type: "WHERE"; - column: string; - comparator: string; - value: ColumnValue; - } - | { - type: "AND"; - whereBlocks: WhereBlock[]; - } - | { - type: "DELETE_FROM"; - table: string; - } - | { - type: "UPDATE"; - table: string; - values: Record; - } - | { - type: "RETURNING"; - columns: string[]; - } - | WhereBlock; - -type WhereBlock = { - type: "WHERE"; - column: string; - comparator: string; - value: ColumnValue; -}; diff --git a/packages/adapter-sqlite/src/utils.ts b/packages/adapter-sqlite/src/utils.ts index b630bb75f..76b7007c2 100644 --- a/packages/adapter-sqlite/src/utils.ts +++ b/packages/adapter-sqlite/src/utils.ts @@ -1,49 +1,29 @@ -import type { KeySchema, SessionSchema, UserSchema } from "lucia-auth"; - -export const transformToSqliteValue = <_Obj extends Record>( - obj: _Obj -): { - [K in keyof _Obj]: _Obj[K] extends boolean - ? number | Exclude<_Obj[K], boolean> - : _Obj[K]; -} => { - return Object.fromEntries( - Object.entries(obj).map(([key, val]) => { - if (typeof val !== "boolean") return [key, val]; - return [key, Number(val)]; - }) - ) as any; -}; - -export const transformDatabaseSession = ( - session: SQLiteSessionSchema -): SessionSchema => { - return { - id: session.id, - user_id: session.user_id, - active_expires: Number(session.active_expires), - idle_expires: Number(session.idle_expires) +const createPreparedStatementHelper = ( + placeholder: (index: number) => string +) => { + const helper = ( + values: Record + ): readonly [fields: string[], placeholders: string[], arguments: any[]] => { + const keys = Object.keys(values); + return [ + keys.map((k) => escapeName(k)), + keys.map((_, i) => placeholder(i)), + keys.map((k) => values[k]) + ] as const; }; + return helper; }; -export const transformDatabaseKey = (key: SQLiteKeySchema): KeySchema => { - return { - id: key.id, - user_id: key.user_id, - primary_key: Boolean(key.primary_key), - hashed_password: key.hashed_password, - expires: key.expires === null ? null : Number(key.expires) - }; +export const escapeName = (val: string) => { + return `${ESCAPE_CHAR}${val}${ESCAPE_CHAR}`; }; -export type SQLiteUserSchema = UserSchema; -export type SQLiteSessionSchema = SessionSchema; -export type SQLiteKeySchema = TransformToSQLiteSchema; +const ESCAPE_CHAR = "`"; -export type ReplaceBooleanWithNumber = Extract extends never - ? T - : Exclude | number; +export const helper = createPreparedStatementHelper(() => "?"); -export type TransformToSQLiteSchema<_Schema extends {}> = { - [K in keyof _Schema]: ReplaceBooleanWithNumber<_Schema[K]>; +export const getSetArgs = (fields: string[], placeholders: string[]) => { + return fields + .map((field, i) => [field, placeholders[i]].join(" = ")) + .join(","); }; diff --git a/packages/adapter-sqlite/test/better-sqlite3/db.ts b/packages/adapter-sqlite/test/better-sqlite3/db.ts deleted file mode 100644 index 486f10bdc..000000000 --- a/packages/adapter-sqlite/test/better-sqlite3/db.ts +++ /dev/null @@ -1,17 +0,0 @@ -import sqlite from "better-sqlite3"; -import dotenv from "dotenv"; -import { resolve } from "path"; -import { LuciaError } from "lucia-auth"; - -import { betterSqlite3 as betterSqlite3Adapter } from "../../src/index.js"; -import { betterSqliteRunner } from "../../src/better-sqlite3/runner.js"; -import { createQueryHandler } from "../index.js"; - -dotenv.config({ - path: `${resolve()}/.env` -}); - -const db = sqlite("test/main.db"); - -export const adapter = betterSqlite3Adapter(db)(LuciaError); -export const queryHandler = createQueryHandler(betterSqliteRunner(db)); diff --git a/packages/adapter-sqlite/test/better-sqlite3/index.ts b/packages/adapter-sqlite/test/better-sqlite3/index.ts index ee388e4f5..24fb4913c 100644 --- a/packages/adapter-sqlite/test/better-sqlite3/index.ts +++ b/packages/adapter-sqlite/test/better-sqlite3/index.ts @@ -1,4 +1,36 @@ -import { testAdapter } from "@lucia-auth/adapter-test"; -import { adapter, queryHandler } from "./db.js"; +import { testAdapter, Database } from "@lucia-auth/adapter-test"; +import { LuciaError } from "lucia"; -testAdapter(adapter, queryHandler); +import { TABLE_NAMES, db } from "../db.js"; +import { betterSqlite3Adapter } from "../../src/drivers/better-sqlite3.js"; +import { escapeName, helper } from "../../src/utils.js"; + +import type { QueryHandler, TableQueryHandler } from "@lucia-auth/adapter-test"; + +const createTableQueryHandler = (tableName: string): TableQueryHandler => { + const ESCAPED_TABLE_NAME = escapeName(tableName); + return { + get: async () => { + return db.prepare(`SELECT * FROM ${ESCAPED_TABLE_NAME}`).all(); + }, + insert: async (value: any) => { + const [fields, placeholders, args] = helper(value); + db.prepare( + `INSERT INTO ${ESCAPED_TABLE_NAME} ( ${fields} ) VALUES ( ${placeholders} )` + ).run(...args); + }, + clear: async () => { + db.exec(`DELETE FROM ${ESCAPED_TABLE_NAME}`); + } + }; +}; + +const queryHandler: QueryHandler = { + user: createTableQueryHandler(TABLE_NAMES.user), + session: createTableQueryHandler(TABLE_NAMES.session), + key: createTableQueryHandler(TABLE_NAMES.key) +}; + +const adapter = betterSqlite3Adapter(db, TABLE_NAMES)(LuciaError); + +testAdapter(adapter, new Database(queryHandler)); diff --git a/packages/adapter-sqlite/test/d1/generate.ts b/packages/adapter-sqlite/test/d1/generate.ts deleted file mode 100644 index 2471174f5..000000000 --- a/packages/adapter-sqlite/test/d1/generate.ts +++ /dev/null @@ -1,19 +0,0 @@ -import dotenv from "dotenv"; -import fs from "fs"; -import path from "path"; -import { resolve } from "path"; - -dotenv.config({ - path: `${resolve()}/.env` -}); - -const tomlFile = ` -node_compat = true - -[[d1_databases]] -binding = "${process.env.D1_DATABASE_BINDING}" -database_name = "${process.env.D1_DATABASE_NAME}" -database_id = "${process.env.D1_DATABASE_ID}" -`; - -fs.writeFileSync(path.resolve("./test/d1/wrangler.toml"), tomlFile); diff --git a/packages/adapter-sqlite/test/d1/index.ts b/packages/adapter-sqlite/test/d1/index.ts index 1d3f2c2a2..74215bdbb 100644 --- a/packages/adapter-sqlite/test/d1/index.ts +++ b/packages/adapter-sqlite/test/d1/index.ts @@ -1,19 +1,47 @@ -import { testAdapter } from "@lucia-auth/adapter-test"; -import { LuciaError } from "lucia-auth"; -import { d1 as d1Adapter } from "../../src/index.js"; -import { d1Runner } from "../../src/d1/runner.js"; -import { createQueryHandler } from "../index.js"; -import { D1Database } from "@cloudflare/workers-types"; - -type Env = { - DB: D1Database; +import { testAdapter, Database } from "@lucia-auth/adapter-test"; +import { LuciaError } from "lucia"; +import { D1Database, D1DatabaseAPI } from "@miniflare/d1"; + +import { d1Adapter } from "../../src/drivers/d1.js"; +import { escapeName, helper } from "../../src/utils.js"; +import { TABLE_NAMES, db } from "../db.js"; + +import type { D1Database as WorkerD1Database } from "@cloudflare/workers-types"; +import type { QueryHandler, TableQueryHandler } from "@lucia-auth/adapter-test"; + +const D1 = new D1Database(new D1DatabaseAPI(db)) as any as WorkerD1Database; + +const createTableQueryHandler = (tableName: string): TableQueryHandler => { + const ESCAPED_TABLE_NAME = escapeName(tableName); + return { + get: async () => { + const { results } = await D1.prepare( + `SELECT * FROM ${ESCAPED_TABLE_NAME}` + ).all(); + return results ?? []; + }, + insert: async (value: any) => { + const [fields, placeholders, args] = helper(value); + await D1.prepare( + `INSERT INTO ${ESCAPED_TABLE_NAME} ( ${fields} ) VALUES ( ${placeholders} )` + ) + .bind(...args) + .run(); + }, + clear: async () => { + await D1.exec(`DELETE FROM ${ESCAPED_TABLE_NAME}`); + } + }; }; -export default { - fetch: async (_: Request, env: Env) => { - const adapter = d1Adapter(env.DB)(LuciaError); - const queryHandler = createQueryHandler(d1Runner(env.DB)); - await testAdapter(adapter, queryHandler, false); - return new Response("Test successful"); - } +const queryHandler: QueryHandler = { + user: createTableQueryHandler(TABLE_NAMES.user), + session: createTableQueryHandler(TABLE_NAMES.session), + key: createTableQueryHandler(TABLE_NAMES.key) }; + +const adapter = d1Adapter(D1, TABLE_NAMES)(LuciaError); + +await testAdapter(adapter, new Database(queryHandler)); + +process.exit(0); diff --git a/packages/adapter-sqlite/test/db.ts b/packages/adapter-sqlite/test/db.ts new file mode 100644 index 000000000..4bb8bd093 --- /dev/null +++ b/packages/adapter-sqlite/test/db.ts @@ -0,0 +1,9 @@ +import sqlite from "better-sqlite3"; + +export const db = sqlite("test/main.db"); + +export const TABLE_NAMES = { + user: "test_user", + session: "user_session", + key: "user_key" +}; diff --git a/packages/adapter-sqlite/test/index.ts b/packages/adapter-sqlite/test/index.ts deleted file mode 100644 index cd66f84f7..000000000 --- a/packages/adapter-sqlite/test/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { LuciaQueryHandler } from "@lucia-auth/adapter-test"; -import { createOperator, Runner } from "../src/query.js"; -import { - transformDatabaseKey, - transformDatabaseSession, - transformToSqliteValue -} from "../src/utils.js"; - -import type { SQLiteKeySchema, SQLiteSessionSchema } from "../src/utils.js"; -import type { TestUserSchema } from "@lucia-auth/adapter-test"; - -export const createQueryHandler = (runner: Runner) => { - const operator = createOperator(runner); - return { - user: { - get: async () => { - return operator.getAll((ctx) => [ - ctx.selectFrom("auth_user", "*") - ]); - }, - insert: async (user) => { - await operator.run((ctx) => [ctx.insertInto("auth_user", user)]); - }, - clear: async () => { - await operator.run((ctx) => [ctx.deleteFrom("auth_user")]); - } - }, - session: { - get: async () => { - const databaseSessions = await operator.getAll( - (ctx) => [ctx.selectFrom("auth_session", "*")] - ); - return databaseSessions.map((val) => transformDatabaseSession(val)); - }, - insert: async (key) => { - await operator.run((ctx) => [ctx.insertInto("auth_session", key)]); - }, - clear: async () => { - await operator.run((ctx) => [ctx.deleteFrom("auth_session")]); - } - }, - key: { - get: async () => { - const databaseKeys = await operator.getAll((ctx) => [ - ctx.selectFrom("auth_key", "*") - ]); - return databaseKeys.map((val) => transformDatabaseKey(val)); - }, - insert: async (key) => { - await operator.run((ctx) => [ - ctx.insertInto("auth_key", transformToSqliteValue(key)) - ]); - }, - clear: async () => { - await operator.run((ctx) => [ctx.deleteFrom("auth_key")]); - } - } - } satisfies LuciaQueryHandler; -}; diff --git a/packages/adapter-sqlite/test/main.db b/packages/adapter-sqlite/test/main.db index ebaa015a2..4e504bb6a 100644 Binary files a/packages/adapter-sqlite/test/main.db and b/packages/adapter-sqlite/test/main.db differ diff --git a/packages/adapter-sqlite/tsconfig.json b/packages/adapter-sqlite/tsconfig.json index 0bf7d70b9..56358e496 100644 --- a/packages/adapter-sqlite/tsconfig.json +++ b/packages/adapter-sqlite/tsconfig.json @@ -11,6 +11,5 @@ "outDir": "./dist", "strict": true }, - "include": ["src"], - "exclude": ["node_modules/", "**/__tests__/*"] + "include": ["src"] } diff --git a/packages/adapter-test/.gitignore b/packages/adapter-test/.gitignore index 4fe613cbf..7a0ae98eb 100644 --- a/packages/adapter-test/.gitignore +++ b/packages/adapter-test/.gitignore @@ -1,3 +1,5 @@ /node_modules /dist -.DS_Store \ No newline at end of file +.DS_Store +.env +*.tgz \ No newline at end of file diff --git a/packages/adapter-test/.npmignore b/packages/adapter-test/.npmignore deleted file mode 100644 index b512c09d4..000000000 --- a/packages/adapter-test/.npmignore +++ /dev/null @@ -1 +0,0 @@ -node_modules \ No newline at end of file diff --git a/packages/adapter-test/README.md b/packages/adapter-test/README.md index c91093f94..b4b43345e 100644 --- a/packages/adapter-test/README.md +++ b/packages/adapter-test/README.md @@ -1,6 +1,6 @@ # Tests for Lucia adapters -Testing package for adapters for Lucia +Testing module for Lucia database adapters. **[Documentation](https://lucia-auth.com/adapters/testing-adapters)** @@ -15,5 +15,3 @@ npm i -D @lucia-auth/adapter-test pnpm add -D @lucia-auth/adapter-test yarn add -D @lucia-auth/adapter-test ``` - -Requires `lucia-auth@0.11.0`. diff --git a/packages/adapter-test/package.json b/packages/adapter-test/package.json index 1c905ff6a..60148e505 100644 --- a/packages/adapter-test/package.json +++ b/packages/adapter-test/package.json @@ -1,21 +1,22 @@ { "name": "@lucia-auth/adapter-test", "version": "3.0.1", - "description": "Tests for database adapters for Lucia", - "main": "index.js", - "types": "index.d.ts", - "module": "index.js", + "description": "Testing module for Lucia database adapters", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "module": "dist/index.js", "type": "module", "files": [ - "**/*" + "/dist/", + "CHANGELOG.md" ], "scripts": { - "build": "shx rm -rf ./dist/* && tsc && shx cp ./package.json ./dist && shx cp ./README.md ./dist && shx cp .npmignore dist", - "auri.publish": "pnpm build && cd dist && pnpm install --no-frozen-lockfile && pnpm publish --no-git-checks --access public && cd ../" + "build": "shx rm -rf ./dist/* && tsc", + "auri.publish": "pnpm build && pnpm publish --no-git-checks --access public" }, "keywords": [ "lucia", - "lucia-auth", + "lucia", "auth", "authentication", "adapter", @@ -29,16 +30,16 @@ "author": "pilcrowonpaper", "license": "MIT", "exports": { - ".": "./index.js" + ".": "./dist/index.js" }, "devDependencies": { - "@types/cli-color": "^2.0.2", - "lucia-auth": "workspace:*" - }, - "dependencies": { - "cli-color": "^2.0.3" + "@types/mocha": "^10.0.1", + "lucia": "latest" }, "peerDependencies": { - "lucia-auth": "^1.3.0" + "lucia": "^2.0.0" + }, + "dependencies": { + "mocha": "^10.2.0" } } diff --git a/packages/adapter-test/src/database.ts b/packages/adapter-test/src/database.ts index 579607b2f..b9a9bd6de 100644 --- a/packages/adapter-test/src/database.ts +++ b/packages/adapter-test/src/database.ts @@ -1,212 +1,125 @@ -import { generateRandomString } from "lucia-auth"; -import { typeError, valueError } from "./validate.js"; -import type { KeySchema, SessionSchema } from "lucia-auth"; +import { generateRandomString } from "lucia/utils"; +import type { KeySchema, SessionSchema, UserSchema } from "lucia"; -export type TestUserSchema = { - id: string; +export type TestUserSchema = UserSchema & { username: string; }; -type QueryHandler = { +export type TestSessionSchema = SessionSchema & { + country: string; +}; + +export type TableQueryHandler< + Schema extends { + id: string; + } = any +> = { get: () => Promise; insert: (data: Schema) => Promise; clear: () => Promise; }; -export type LuciaQueryHandler = { - user?: QueryHandler; - session?: QueryHandler; - key?: QueryHandler; +export type QueryHandler = { + user?: TableQueryHandler; + session?: TableQueryHandler; + key?: TableQueryHandler; }; export class Database { - private readonly queryHandler: LuciaQueryHandler; - public user = () => { - return new User(this.queryHandler); - }; - public clear = async () => { - await this.queryHandler.key?.clear(); - await this.queryHandler.session?.clear(); - await this.queryHandler.user?.clear(); - }; - constructor(queryHandler: LuciaQueryHandler) { - this.queryHandler = queryHandler; - } -} - -type ExtractQueryHandlerSchema = Q extends QueryHandler - ? Schema - : never; + private readonly queryHandler: QueryHandler; -class Model> { - public value: ExtractQueryHandlerSchema; - protected readonly name: string; - protected readonly queryHandler: LuciaQueryHandler; - private storeQueryHandler: LuciaQueryHandler[StoreName]; - private readonly parent: Model[]; - constructor( - name: StoreName, - queryHandler: LuciaQueryHandler, - value: ExtractQueryHandlerSchema, - parent: Model[] = [] - ) { - this.name = name; - this.value = value; + constructor(queryHandler: QueryHandler) { this.queryHandler = queryHandler; - this.storeQueryHandler = queryHandler[name]; - this.parent = parent; } - public commit = async () => { - for (const parentModel of this.parent) { - await parentModel.commit(); + + public user = () => { + const userQueryHandler = this.queryHandler["user"]; + if (!userQueryHandler) { + throw new Error("No query handler provided for 'user'"); } - await this.storeQueryHandler?.insert(this.value as any); + return new Table(userQueryHandler); }; - private safeCompare = (target: unknown) => { - if (typeof target !== "object" || target === null) - throw typeError(target, "object"); - for (const [refKey, refValue] of Object.entries(this.value) as [any, any]) { - if (target[refKey as keyof Object] !== refValue) { - return false; - } + + public session = () => { + const sessionQueryHandler = this.queryHandler["session"]; + if (!sessionQueryHandler) { + throw new Error("No query handler provided for 'session'"); } - return true; + return new Table(sessionQueryHandler); }; - public compare = (target: unknown) => { - const isEqual = this.safeCompare(target); - if (isEqual) return; - throw valueError(target, this.value, "Target was not the expected value"); - }; - public find = (target: unknown) => { - if (!Array.isArray(target)) { - throw typeError(target, "array"); - } - for (const value of target) { - const isEqual = this.safeCompare(value); - if (isEqual) return; + + public key = () => { + const keyQueryHandler = this.queryHandler["key"]; + if (!keyQueryHandler) { + throw new Error("No query handler provided for 'key'"); } - throw valueError( - target, - this.value, - "Target did not include the expected value" - ); + return new Table(keyQueryHandler); }; - public exists = async () => { - const databaseData = (await this.storeQueryHandler?.get()) ?? []; - const existsInDatabase = databaseData.some(this.safeCompare); - if (existsInDatabase) return; - console.log("target:"); - console.dir(this.value, { - depth: null - }); - console.log("store"); - console.dir(databaseData, { - depth: null - }); - throw new Error(`Target not found in store ${this.name}`); - }; - public notExits = async () => { - const databaseData = (await this.storeQueryHandler?.get()) ?? []; - const existsInDatabase = databaseData.some(this.safeCompare); - if (!existsInDatabase) return; - console.log("target:"); - console.dir(this.value, { - depth: null - }); - console.log("store"); - console.dir(databaseData, { - depth: null - }); - throw new Error(`Target found in store ${this.name}`); - }; - public update = ( - value: Partial> - ) => { - this.value = { ...this.value, ...value }; - }; -} -class User extends Model<"user"> { - public session = () => { - return new Session(this.queryHandler, [this], { - userId: this.value.id - }); - }; - public key = (option: { - primary: boolean; - passwordDefined: boolean; - oneTime: boolean; - }) => { - return new Key(this.queryHandler, [this], { - userId: this.value.id, - ...option - }); - }; - constructor( - queryHandler: LuciaQueryHandler, - options?: { - userId?: string; - username?: string; - } - ) { + public generateUser = (options?: { + userId?: string; + username?: string; + }): TestUserSchema => { const userId = options?.userId ?? generateRandomString(8); - const username = options?.username ?? `user_${generateRandomString(4)}`; - super("user", queryHandler, { + const username = options?.username ?? generateRandomString(4); + return { id: userId, username - }); - } -} - -class Session extends Model<"session"> { - constructor( - queryHandler: LuciaQueryHandler, - parent: Model[], - options: { - userId: string; + }; + }; + public generateSession = ( + userId: string | null, + options?: { + id?: string; + country?: string; } - ) { + ): TestSessionSchema => { const activeExpires = new Date().getTime() + 1000 * 60 * 60 * 8; - super( - "session", - queryHandler, - { - user_id: options.userId, - id: `at_${generateRandomString(40)}`, - active_expires: activeExpires, - idle_expires: activeExpires + 1000 * 60 * 60 * 24 - }, - parent - ); - } -} + return { + user_id: userId ?? generateRandomString(8), + id: options?.id ?? `at_${generateRandomString(40)}`, + active_expires: activeExpires, + idle_expires: activeExpires + 1000 * 60 * 60 * 24, + country: options?.country ?? "XX" + }; + }; -class Key extends Model<"key"> { - constructor( - queryHandler: LuciaQueryHandler, - parent: Model[], - options: { - userId: string; - primary: boolean; - passwordDefined: boolean; - oneTime: boolean; + public generateKey = ( + userId: string | null, + options?: { + id?: string; } - ) { - const DURATION_SEC = 60 * 60; - const oneTimeExpires = options.oneTime - ? new Date().getTime() + DURATION_SEC * 1000 - : null; - super( - "key", - queryHandler, - { - id: `test:${options.userId}@example.com`, - user_id: options.userId, - primary_key: options.primary, - hashed_password: options.passwordDefined ? "HASHED" : null, - expires: oneTimeExpires - }, - parent - ); + ): KeySchema => { + const keyUserId = userId ?? generateRandomString(8); + return { + id: options?.id ?? generateRandomString(30), + user_id: keyUserId, + hashed_password: null + }; + }; + + public clear = async () => { + await this.queryHandler.key?.clear(); + await this.queryHandler.session?.clear(); + await this.queryHandler.user?.clear(); + }; +} + +class Table<_Schema extends { id: string }> { + protected readonly queryHandler: TableQueryHandler<_Schema>; + constructor(queryHandler: TableQueryHandler<_Schema>) { + this.queryHandler = queryHandler; } + public insert = async (...values: _Schema[]) => { + for (const value of values) { + await this.queryHandler.insert(value); + } + }; + public get = async (id: string) => { + const result = await this.queryHandler.get(); + return result.find((val) => val.id === id) ?? null; + }; + public getAll = async () => { + return await this.queryHandler.get(); + }; } diff --git a/packages/adapter-test/src/index.ts b/packages/adapter-test/src/index.ts index bf7234f02..e6ff5ac66 100644 --- a/packages/adapter-test/src/index.ts +++ b/packages/adapter-test/src/index.ts @@ -1,4 +1,10 @@ -export { testAdapter } from "./tests/index.js"; +export { testAdapter } from "./tests/main.js"; export { testSessionAdapter } from "./tests/session.js"; -export { testUserAdapter } from "./tests/user.js"; -export type { LuciaQueryHandler, TestUserSchema } from "./database.js"; +export { Database } from "./database.js"; + +export type { + QueryHandler, + TableQueryHandler, + TestUserSchema, + TestSessionSchema +} from "./database.js"; diff --git a/packages/adapter-test/src/lucia.d.ts b/packages/adapter-test/src/lucia.d.ts index 0184570ee..1f534bf88 100644 --- a/packages/adapter-test/src/lucia.d.ts +++ b/packages/adapter-test/src/lucia.d.ts @@ -1,7 +1,10 @@ -/// +/// declare namespace Lucia { type Auth = any; - type UserAttributes = { + type DatabaseUserAttributes = { username: string; }; + type DatabaseSessionAttributes = { + country: string; + }; } diff --git a/packages/adapter-test/src/test.ts b/packages/adapter-test/src/test.ts index 2fc24ea19..2d47d7167 100644 --- a/packages/adapter-test/src/test.ts +++ b/packages/adapter-test/src/test.ts @@ -1,27 +1,47 @@ -import clc from "cli-color"; +type Test = (name: string, fn: () => Promise) => Promise; +type Skip = () => void; -export const test = async ( +let passedCount = 0; + +const test: Test = async (name, fn) => { + try { + await fn(); + passedCount += 1; + console.log(` \x1B[32m✓ \x1B[0;2m${name}\x1B[0m`); + await afterEachFn(); + } catch (error) { + console.log(` \x1B[31m✗ \x1B[0;2m${name}\x1B[0m`); + throw error; + } +}; + +const skip: Skip = () => { + console.log(` \x1B[33m! \x1B[0;2mSkipped tests\x1B[0m`); +}; + +export const method = async ( name: string, - description: string, - func: () => Promise + runTests: (test: Test, skip: Skip) => Promise ) => { + console.log(`\n \x1B[36m${name}\x1B[0m`); + + await runTests(test, skip); +}; + +export const start = () => { console.log( - `\n${clc.bold.blue("[Test]")} ${clc.bold(name)} : ${description}` + `\n\x1B[38;5;63;1m[start] \x1B[0;2m Running adapter testing module\x1B[0m` ); - try { - await func(); - console.log(`${clc.green.bold("[Success]")} ${name}`); - } catch (e) { - const error = e as Error; - console.error(`${clc.red("[Error]")} ${error.message}`); - console.error(`${clc.bold.red("[Failed]")} ${name}`); - throw new Error(); - } }; -export const end = () => { - console.log(`${clc.green.bold("Success!")} Completed all tests`); - process.exit(); +export const finish = () => { + console.log( + `\n\x1B[32;1m[success] \x1B[0;2m Adapter passed \x1B[3m${passedCount}\x1B[23m tests\x1B[0m\n` + ); }; -export const INVALID_INPUT = "INVALID_INPUT"; +let afterEachFn = async () => {}; + +export const afterEach = (fn: () => Promise) => { + afterEachFn = fn; +}; diff --git a/packages/adapter-test/src/tests/index.ts b/packages/adapter-test/src/tests/index.ts deleted file mode 100644 index e8876567a..000000000 --- a/packages/adapter-test/src/tests/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Adapter } from "lucia-auth"; -import { test, end, INVALID_INPUT } from "./../test.js"; -import { testUserAdapter } from "./user.js"; -import { testSessionAdapter } from "./session.js"; -import { Database, type LuciaQueryHandler } from "../database.js"; -import { isNull } from "../validate.js"; - -export const testAdapter = async ( - adapter: Adapter, - queryHandler: LuciaQueryHandler, - endProcess = true -) => { - const database = new Database(queryHandler); - const clearAll = database.clear; - await clearAll(); - await testUserAdapter(adapter, queryHandler, false); - await testSessionAdapter(adapter, queryHandler, false); - await testOptionalMethods(adapter, queryHandler, false); - if (endProcess) { - end(); - } -}; - -const testOptionalMethods = async ( - adapter: Adapter, - queryHandler: LuciaQueryHandler, - endProcess = true -) => { - const database = new Database(queryHandler); - const clearAll = database.clear; - await test( - "getSessionAndUserBySessionId()", - "Return the correct user and session", - async () => { - if (!adapter.getSessionAndUserBySessionId) return; - const user = database.user(); - const session = user.session(); - await session.commit(); // this will set user as well - const result = await adapter.getSessionAndUserBySessionId( - session.value.id - ); - user.compare(result?.user); - session.compare(result?.session); - await clearAll(); - } - ); - await test( - "getSessionAndUserBySessionId()", - "Return null if session id is invalid", - async () => { - if (!adapter.getSessionAndUserBySessionId) return; - const result = await adapter.getSessionAndUserBySessionId(INVALID_INPUT); - isNull(result); - await clearAll(); - } - ); - await clearAll(); - if (endProcess) { - end(); - } -}; diff --git a/packages/adapter-test/src/tests/main.ts b/packages/adapter-test/src/tests/main.ts new file mode 100644 index 000000000..169f6b7d5 --- /dev/null +++ b/packages/adapter-test/src/tests/main.ts @@ -0,0 +1,346 @@ +import { LuciaError } from "lucia"; +import assert from "node:assert/strict"; +import { start, finish, method, afterEach } from "../test.js"; + +import type { Database } from "../database.js"; +import type { Adapter, SessionSchema, KeySchema, UserSchema } from "lucia"; + +export const testAdapter = async (adapter: Adapter, database: Database) => { + await database.clear(); + + const User = database.user(); + const Session = database.session(); + const Key = database.key(); + + afterEach(database.clear); + + start(); + + await method("getUser()", async (test) => { + await test("Returns target user", async () => { + const user = database.generateUser(); + await User.insert(user); + const result = await adapter.getUser(user.id); + assert.deepStrictEqual(result, user); + }); + await test("Returns null if invalid target user id", async () => { + const user = database.generateUser(); + await User.insert(user); + const result = await adapter.getUser("*"); + assert.deepStrictEqual(result, null); + }); + }); + + await method("setUser()", async (test) => { + await test("Inserts user only", async () => { + const user = database.generateUser(); + await adapter.setUser(user, null); + const storedUser = await User.get(user.id); + assert.deepStrictEqual(storedUser, user); + }); + await test("Inserts user and key", async () => { + const user = database.generateUser(); + const key = database.generateKey(user.id); + await adapter.setUser(user, key); + const storedUser = await User.get(user.id); + const storedKey = await Key.get(key.id); + assert.deepStrictEqual(storedUser, user); + assert.deepStrictEqual(storedKey, key); + }); + await test("Throws DUPLICATE_KEY_ID on duplicate key id", async () => { + const user1 = database.generateUser(); + const key1 = database.generateKey(user1.id); + await User.insert(user1); + await Key.insert(key1); + const user2 = database.generateUser(); + const key2 = database.generateKey(user2.id, { + id: key1.id + }); + await assert.rejects(async () => { + await adapter.setUser(user2, key2); + }, new LuciaError("AUTH_DUPLICATE_KEY_ID")); + }); + + await test("Does not insert key if errors", async () => { + const user1 = database.generateUser(); + const key1 = database.generateKey(user1.id); + await User.insert(user1); + await Key.insert(key1); + const user2 = database.generateUser(); + const key2 = database.generateKey(user2.id, { + id: key1.id + }); + await assert.rejects(async () => { + await adapter.setUser(user2, key2); + }, new LuciaError("AUTH_DUPLICATE_KEY_ID")); + const storedUsers = await User.getAll(); + assert.deepStrictEqual(storedUsers, [user1]); + }); + }); + + await method("deleteUser()", async (test) => { + await test("Deletes target user", async () => { + const user1 = database.generateUser(); + const user2 = database.generateUser(); + await User.insert(user1, user2); + await adapter.deleteUser(user2.id); + const storedUsers = await User.getAll(); + assert.deepStrictEqual(storedUsers, [user1]); + }); + }); + + await method("updateUser()", async (test) => { + await test("Updates user 'username' field", async () => { + const user = database.generateUser(); + await User.insert(user); + await adapter.updateUser(user.id, { + username: "Y" + }); + const updatedUser = { + ...user, + username: "Y" + } satisfies UserSchema; + const storedUser = await User.get(user.id); + assert.deepStrictEqual(storedUser, updatedUser); + }); + }); + + await method("getKey()", async (test) => { + await test("Returns target key", async () => { + const user = database.generateUser(); + const key = database.generateKey(user.id); + await User.insert(user); + await Key.insert(key); + const result = await adapter.getKey(key.id); + assert.deepStrictEqual(result, key); + }); + await test("Returns null if invalid target key id", async () => { + const result = await adapter.getKey("*"); + assert.deepStrictEqual(result, null); + }); + }); + + await method("setKey()", async (test) => { + await test("Inserts key", async () => { + const user = database.generateUser(); + const key = database.generateKey(user.id); + await User.insert(user); + await adapter.setKey(key); + const storedKey = await Key.get(key.id); + assert.deepStrictEqual(storedKey, key); + }); + await test("Throws AUTH_DUPLICATE_KEY_ID on duplicate key id", async () => { + const user = database.generateUser(); + const key1 = database.generateKey(user.id); + await User.insert(user); + await adapter.setKey(key1); + const key2 = database.generateKey(user.id, { + id: key1.id + }); + await assert.rejects(async () => { + await adapter.setKey(key2); + }, new LuciaError("AUTH_DUPLICATE_KEY_ID")); + }); + await test("Optionally throws AUTH_INVALID_USER_ID on invalid user id", async () => { + const key = database.generateKey(null); + try { + await adapter.setKey(key); + } catch (e) { + assert.deepStrictEqual(e, new LuciaError("AUTH_INVALID_USER_ID")); + } + }); + }); + + await method("updateKey", async (test) => { + await test("Updates key 'hashed_password' field", async () => { + const user = database.generateUser(); + const key = database.generateKey(user.id); + await User.insert(user); + await Key.insert(key); + await adapter.updateKey(key.id, { + hashed_password: "HASHED" + }); + const updatedKey = { + ...key, + hashed_password: "HASHED" + } satisfies KeySchema; + const storedKey = await Key.get(key.id); + assert.deepStrictEqual(storedKey, updatedKey); + }); + }); + + await method("getKeysByUserId()", async (test) => { + await test("Returns keys with target user id", async () => { + const user1 = database.generateUser(); + const user2 = database.generateUser(); + const key1 = database.generateKey(user1.id); + const key2 = database.generateKey(user2.id); + await User.insert(user1, user2); + await Key.insert(key1, key2); + const result = await adapter.getKeysByUserId(user1.id); + assert.deepStrictEqual(result, [key1]); + }); + await test("Returns an empty array if none matches target", async () => { + const user = database.generateUser(); + const key = database.generateKey(user.id); + await User.insert(user); + await Key.insert(key); + const result = await adapter.getKeysByUserId("*"); + assert.deepStrictEqual(result, []); + }); + }); + + await method("deleteKeysByUserId()", async (test) => { + await test("Deletes keys with target user id", async () => { + const user1 = database.generateUser(); + const user2 = database.generateUser(); + const key1 = database.generateKey(user1.id); + const key2 = database.generateKey(user2.id); + await User.insert(user1, user2); + await Key.insert(key1, key2); + await adapter.deleteKeysByUserId(user1.id); + const storedKeys = await Key.getAll(); + assert.deepStrictEqual(storedKeys, [key2]); + }); + }); + + await method("deleteKey()", async (test) => { + await test("Deletes target key", async () => { + const user = database.generateUser(); + const key1 = database.generateKey(user.id); + const key2 = database.generateKey(user.id); + await User.insert(user); + await Key.insert(key1); + await Key.insert(key2); + await adapter.deleteKey(key1.id); + const storedKeys = await Key.getAll(); + assert.deepStrictEqual(storedKeys, [key2]); + }); + }); + + await method("getSession()", async (test) => { + await test("Returns target session", async () => { + const user = database.generateUser(); + const session = database.generateSession(user.id); + await User.insert(user); + await Session.insert(session); + const sessionResult = await adapter.getSession(session.id); + assert.deepStrictEqual(sessionResult, session); + }); + await test("Returns null if invalid target session id", async () => { + const session = await adapter.getSession("*"); + assert.deepStrictEqual(session, null); + }); + }); + + await method("getSessionsByUserId()", async (test) => { + await test("Return sessions with target user id", async () => { + const user1 = database.generateUser(); + const user2 = database.generateUser(); + const session1 = database.generateSession(user1.id); + const session2 = database.generateSession(user2.id); + await User.insert(user1, user2); + await Session.insert(session1, session2); + const result = await adapter.getSessionsByUserId(user1.id); + assert.deepStrictEqual(result, [session1]); + }); + await test("Returns an empty array if none matches target", async () => { + const user = database.generateUser(); + const session = database.generateSession(user.id); + await User.insert(user); + await Session.insert(session); + const result = await adapter.getSessionsByUserId("*"); + assert.deepStrictEqual(result, []); + }); + }); + + await method("setSession()", async (test) => { + await test("Inserts session", async () => { + const user = database.generateUser(); + await User.insert(user); + const session = database.generateSession(user.id); + await adapter.setSession(session); + const storedSession = await Session.get(session.id); + assert.deepStrictEqual(storedSession, session); + }); + await test("Optionally throws AUTH_INVALID_USER_ID on invalid user id", async () => { + const session = database.generateSession(null); + try { + await adapter.setSession(session); + } catch (e) { + assert.deepStrictEqual(e, new LuciaError("AUTH_INVALID_USER_ID")); + } + }); + }); + + await method("deleteSession()", async (test) => { + await test("Deletes target session", async () => { + const user = database.generateUser(); + const session1 = database.generateSession(user.id); + const session2 = database.generateSession(user.id); + await User.insert(user); + await Session.insert(session1); + await Session.insert(session2); + await adapter.deleteSession(session1.id); + const storedSessions = await Session.getAll(); + assert.deepStrictEqual(storedSessions, [session2]); + }); + }); + + await method("deleteSessionsByUserId()", async (test) => { + await test("Deletes sessions with target user id", async () => { + const user1 = database.generateUser(); + const user2 = database.generateUser(); + const session1 = database.generateSession(user1.id); + const session2 = database.generateSession(user2.id); + await User.insert(user1, user2); + await Session.insert(session1, session2); + await adapter.deleteSessionsByUserId(user1.id); + const storedSessions = await Session.getAll(); + assert.deepStrictEqual(storedSessions, [session2]); + }); + }); + + await method("updateSession()", async (test) => { + await test("Updates session 'country' field", async () => { + const user = database.generateUser(); + const session = database.generateSession(user.id); + await User.insert(user); + await Session.insert(session); + await adapter.updateSession(session.id, { + country: "YY" + }); + const expectedSession = { + ...session, + country: "YY" + } satisfies SessionSchema; + const storedSession = await Session.get(expectedSession.id); + assert.deepStrictEqual(storedSession, expectedSession); + }); + }); + + await method("getSessionAndUser()", async (test, skip) => { + if (!adapter.getSessionAndUser) return skip(); + await test("Returns target session and user", async () => { + if (!adapter.getSessionAndUser) return; + const user = database.generateUser(); + const session = database.generateSession(user.id); + await User.insert(user); + await Session.insert(session); + const [sessionResult, userResult] = await adapter.getSessionAndUser( + session.id + ); + assert.deepStrictEqual(sessionResult, session); + assert.deepStrictEqual(userResult, user); + }); + + await test("Returns null, null if invalid target session id", async () => { + if (!adapter.getSessionAndUser) return; + const [sessionResult, userResult] = await adapter.getSessionAndUser("*"); + assert.deepStrictEqual(sessionResult, null); + assert.deepStrictEqual(userResult, null); + }); + }); + + finish(); +}; diff --git a/packages/adapter-test/src/tests/session.ts b/packages/adapter-test/src/tests/session.ts index 0fe3dff86..9b2381196 100644 --- a/packages/adapter-test/src/tests/session.ts +++ b/packages/adapter-test/src/tests/session.ts @@ -1,126 +1,99 @@ -import type { SessionAdapter } from "lucia-auth"; -import { test, end } from "../test.js"; -import { Database, type LuciaQueryHandler } from "../database.js"; -import { expectErrorMessage, isEmptyArray, isNull } from "../validate.js"; +import { start, finish, method, afterEach } from "../test.js"; +import assert from "node:assert/strict"; -const INVALID_INPUT = "INVALID_INPUT"; +import type { SessionSchema, SessionAdapter } from "lucia"; +import type { Database } from "../database.js"; export const testSessionAdapter = async ( adapter: SessionAdapter, - queryHandler: LuciaQueryHandler, - endProcess = true + database: Database ) => { - const database = new Database(queryHandler); - const clearAll = database.clear; - await test("getSession()", "Return the correct session", async () => { - const session = database.user().session(); - await session.commit(); - const result = await adapter.getSession(session.value.id); - session.compare(result); - await clearAll(); + const Session = database.session(); + + afterEach(database.clear); + + start(); + + await method("getSession()", async (test) => { + await test("Returns target session", async () => { + const session = database.generateSession(null); + await Session.insert(session); + const sessionResult = await adapter.getSession(session.id); + assert.deepStrictEqual(sessionResult, session); + }); + await test("Returns null if invalid target session id", async () => { + const session = await adapter.getSession("*"); + assert.deepStrictEqual(session, null); + }); + }); + + await method("getSessionsByUserId()", async (test) => { + await test("Return sessions with target user id", async () => { + const user1 = database.generateUser(); + const user2 = database.generateUser(); + const session1 = database.generateSession(user1.id); + const session2 = database.generateSession(user2.id); + await Session.insert(session1); + await Session.insert(session2); + const result = await adapter.getSessionsByUserId(user1.id); + assert.deepStrictEqual(result, [session1]); + }); + await test("Returns an empty array if none matches target", async () => { + const result = await adapter.getSessionsByUserId("*"); + assert.deepStrictEqual(result, []); + }); + }); + + await method("setSession()", async (test) => { + await test("Inserts session", async () => { + const session = database.generateSession(null); + await adapter.setSession(session); + const storedSession = await Session.get(session.id); + assert.deepStrictEqual(storedSession, session); + }); }); - await test( - "getSession()", - "Return null if session id is invalid", - async () => { - const session = await adapter.getSession(INVALID_INPUT); - isNull(session); - await clearAll(); - } - ); - await test( - "getSessionsByUserId()", - "Return the correct session", - async () => { - const session1 = database.user().session(); - await session1.commit(); - const session2 = database.user().session(); - await session2.commit(); - const result = await adapter.getSessionsByUserId(session1.value.user_id); - session1.find(result); - await clearAll(); - } - ); - await test( - "getSessionsByUserId()", - "Returns an empty array if no sessions exist", - async () => { - const result = await adapter.getSessionsByUserId(INVALID_INPUT); - isEmptyArray(result); - await clearAll(); - } - ); - await test( - "setSession()", - "Insert a user's session into session table", - async () => { - const user = database.user(); - await user.commit(); - const session = user.session(); - await adapter.setSession(session.value); - await session.exists(); - await clearAll(); - } - ); - await test( - "deleteSessionsByUserId()", - "Delete a user's session from session table", - async () => { - const session1 = database.user().session(); - await session1.commit(); - const session2 = database.user().session(); - await session2.commit(); - await adapter.deleteSessionsByUserId(session1.value.user_id); - await session1.notExits(); - await session2.exists(); - await clearAll(); - } - ); - await test( - "deleteSession()", - "Delete a user's session from session table", - async () => { - const session1 = database.user().session(); - await session1.commit(); - const session2 = database.user().session(); - await session2.commit(); - await adapter.deleteSession(session1.value.id); - await session1.notExits(); - await session2.exists(); - await clearAll(); - } - ); - await test( - "setSession()", - "Throw AUTH_INVALID_USER_ID if user id doesn't exist", - async () => { - const session = database.user().session(); - await expectErrorMessage(async () => { - await adapter.setSession(session.value); - }, "AUTH_INVALID_USER_ID"); - await clearAll(); - } - ); - await test( - "setSession()", - "Throw AUTH_DUPLICATE_SESSION_ID if session id is already in use", - async () => { - const session1 = database.user().session(); - await session1.commit(); - const user = database.user(); - await user.commit(); - const session2 = user.session(); - session2.update({ - id: session1.value.id + + await method("deleteSession()", async (test) => { + await test("Deletes target session", async () => { + const user = database.generateUser(); + const session1 = database.generateSession(user.id); + const session2 = database.generateSession(user.id); + await Session.insert(session1); + await Session.insert(session2); + await adapter.deleteSession(session1.id); + const storedSessions = await Session.getAll(); + assert.deepStrictEqual(storedSessions, [session2]); + }); + }); + + await method("deleteSessionsByUserId()", async (test) => { + await test("Deletes sessions with target user id", async () => { + const user1 = database.generateUser(); + const user2 = database.generateUser(); + const session1 = database.generateSession(user1.id); + const session2 = database.generateSession(user2.id); + await Session.insert(session1, session2); + await adapter.deleteSessionsByUserId(user1.id); + const storedSessions = await Session.getAll(); + assert.deepStrictEqual(storedSessions, [session2]); + }); + }); + + await method("updateSession()", async (test) => { + await test("Updates session 'country' field", async () => { + const session = database.generateSession(null); + await Session.insert(session); + await adapter.updateSession(session.id, { + country: "YY" }); - await expectErrorMessage(async () => { - await adapter.setSession(session2.value); - }, "AUTH_DUPLICATE_SESSION_ID"); - await clearAll(); - } - ); - await clearAll(); - if (endProcess) { - end(); - } + const expectedSession = { + ...session, + country: "YY" + } satisfies SessionSchema; + const storedSession = await Session.get(expectedSession.id); + assert.deepStrictEqual(storedSession, expectedSession); + }); + }); + + finish(); }; diff --git a/packages/adapter-test/src/tests/user.ts b/packages/adapter-test/src/tests/user.ts deleted file mode 100644 index 2bb9eca59..000000000 --- a/packages/adapter-test/src/tests/user.ts +++ /dev/null @@ -1,474 +0,0 @@ -import { LuciaError, type UserAdapter } from "lucia-auth"; -import { test, end, INVALID_INPUT } from "../test.js"; -import { Database, type LuciaQueryHandler } from "../database.js"; -import { - isNull, - isEmptyArray, - expectErrorMessage, - expectError -} from "../validate.js"; - -export const testUserAdapter = async ( - adapter: UserAdapter, - queryHandler: LuciaQueryHandler, - endProcess = true -) => { - const database = new Database(queryHandler); - const clearAll = database.clear; - await clearAll(); - await test("getUser()", "Return the correct user", async () => { - const user = database.user(); - await user.commit(); - const returnedUser = await adapter.getUser(user.value.id); - user.compare(returnedUser); - await clearAll(); - }); - await test("getUser()", "Return null if user id is invalid", async () => { - const result = await adapter.getUser(INVALID_INPUT); - isNull(result); - await clearAll(); - }); - await test("setUser()", "Insert user", async () => { - const user = database.user(); - await adapter.setUser( - user.value.id, - { username: user.value.username }, - null - ); - await user.exists(); - await clearAll(); - }); - await test( - "setUser()", - "Insert user - Return the created user or void", - async () => { - const user = database.user(); - const result = await adapter.setUser( - user.value.id, - { username: user.value.username }, - null - ); - if (result !== undefined) { - user.compare(result); - } - await clearAll(); - } - ); - await test("setUser()", "Insert user and persistent key", async () => { - const user = database.user(); - const key = user.key({ - primary: true, - passwordDefined: true, - oneTime: false - }); - await adapter.setUser( - user.value.id, - { username: user.value.username }, - key.value - ); - await user.exists(); - await clearAll(); - }); - await test( - "setUser()", - "Insert user and persistent key - Return created user or void", - async () => { - const user = database.user(); - const key = user.key({ - primary: true, - passwordDefined: true, - oneTime: false - }); - const result = await adapter.setUser( - user.value.id, - { username: user.value.username }, - key.value - ); - if (result !== undefined) { - user.compare(result); - } - await clearAll(); - } - ); - await test( - "setUser()", - "Throw AUTH_DUPLICATE_KEY_ID if key already exists", - async () => { - const refKey = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await refKey.commit(); - const user = database.user(); - const key = user.key({ - primary: true, - passwordDefined: true, - oneTime: false - }); - key.update({ - id: refKey.value.id - }); - await expectErrorMessage(async () => { - await adapter.setUser( - user.value.id, - { username: user.value.username }, - key.value - ); - }, "AUTH_DUPLICATE_KEY_ID"); - await clearAll(); - } - ); - await test("setUser()", "User not stored if key insert errors", async () => { - const refKey = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await refKey.commit(); - const user = database.user(); - const key = user.key({ - primary: true, - passwordDefined: true, - oneTime: false - }); - key.update({ - id: refKey.value.id - }); - await expectError(async () => { - await adapter.setUser( - user.value.id, - { username: user.value.username }, - key.value - ); - }); - await key.notExits(); - await clearAll(); - }); - await test("deleteUser()", "Delete a user from user table", async () => { - const user1 = database.user(); - await user1.commit(); - const user2 = database.user(); - await user2.commit(); - await adapter.deleteUser(user1.value.id); - await user1.notExits(); - await user2.exists(); - await clearAll(); - }); - await test("updateUserAttributes()", "Update user attributes", async () => { - const user = database.user(); - await user.commit(); - user.update({ - username: "UPDATED" - }); - await adapter.updateUserAttributes(user.value.id, { - username: user.value.username - }); - await user.exists(); - await clearAll(); - }); - await test( - "updateUserAttributes()", - "Returns updated user or void", - async () => { - const user = database.user(); - await user.commit(); - user.update({ - username: "UPDATED" - }); - const returnedUser = await adapter.updateUserAttributes(user.value.id, { - username: user.value.username - }); - if (returnedUser !== undefined) { - user.compare(returnedUser); - } - await clearAll(); - } - ); - await test( - "updateUserAttributes()", - "Throw INVALID_USER_ID or return void if user id is invalid", - async () => { - const user = database.user(); - await user.commit(); - expectErrorMessage(async () => { - const returnedUser = await adapter.updateUserAttributes(INVALID_INPUT, { - username: user.value.username - }); - if (returnedUser === undefined) { - throw new LuciaError("AUTH_INVALID_USER_ID"); - } - }, "AUTH_INVALID_USER_ID"); - await clearAll(); - } - ); - await test("getKey()", "Returns the correct persistent key", async () => { - const key = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await key.commit(); - const result = await adapter.getKey(key.value.id, async () => false); - key.compare(result); - await clearAll(); - }); - await test("getKey()", "Returns the correct single use key", async () => { - const key = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: true - }); - await key.commit(); - const result = await adapter.getKey(key.value.id, async () => false); - key.compare(result); - await clearAll(); - }); - await test("getKey()", "Return null if key id is invalid", async () => { - const result = await adapter.getUser(INVALID_INPUT); - isNull(result); - await clearAll(); - }); - await test( - "setKey()", - "Insert a new persistent key with password", - async () => { - const user = database.user(); - await user.commit(); - const key = user.key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await adapter.setKey(key.value); - await key.exists(); - await clearAll(); - } - ); - await test( - "setKey()", - "Insert a new persistent key with null password", - async () => { - const user = database.user(); - await user.commit(); - const key = user.key({ - primary: false, - passwordDefined: false, - oneTime: false - }); - await adapter.setKey(key.value); - await key.exists(); - await clearAll(); - } - ); - await test( - "setKey()", - "Insert a new single use key with null password", - async () => { - const user = database.user(); - await user.commit(); - const key = user.key({ - primary: false, - passwordDefined: false, - oneTime: true - }); - await adapter.setKey(key.value); - await key.exists(); - await clearAll(); - } - ); - await test("setKey()", "Insert a new primary persistent key", async () => { - const user = database.user(); - await user.commit(); - const key = user.key({ - primary: true, - passwordDefined: false, - oneTime: false - }); - await adapter.setKey(key.value); - await key.exists(); - await clearAll(); - }); - await test( - "setKey()", - "Throw AUTH_INVALID_USER_ID if user id is invalid", - async () => { - const key = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await expectErrorMessage(async () => { - await adapter.setKey(key.value); - }, "AUTH_INVALID_USER_ID"); - await clearAll(); - } - ); - await test( - "setKey()", - "Throw AUTH_DUPLICATE_KEY_ID if key already exists", - async () => { - const key = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await key.commit(); - await expectErrorMessage(async () => { - await adapter.setKey(key.value); - }, "AUTH_DUPLICATE_KEY_ID"); - await clearAll(); - } - ); - await test("getKeysByUserId()", "Returns the correct key", async () => { - const key1 = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - const key2 = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await key1.commit(); - await key2.commit(); - const sessions = await adapter.getKeysByUserId(key1.value.user_id); - key1.find(sessions); - await clearAll(); - }); - await test( - "getKeysByUserId()", - "Returns an empty array if no sessions exist", - async () => { - const keys = await adapter.getKeysByUserId(INVALID_INPUT); - isEmptyArray(keys); - await clearAll(); - } - ); - await test("updateKeyPassword()", "Updates key password", async () => { - const key = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await key.commit(); - key.update({ - hashed_password: "UPDATED" - }); - await adapter.updateKeyPassword(key.value.id, key.value.hashed_password); - await key.exists(); - await clearAll(); - }); - await test( - "updateKeyPassword()", - "Throw AUTH_INVALID_KEY_ID if key id is invalid", - async () => { - const key = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await key.commit(); - key.update({ - hashed_password: "UPDATED" - }); - const returnedKey = await adapter.updateKeyPassword( - key.value.id, - key.value.hashed_password - ); - if (returnedKey !== undefined) { - key.compare(returnedKey); - } - await clearAll(); - } - ); - await test( - "updateKeyPassword()", - "Throw AUTH_INVALID_KEY_ID or return void if key id is invalid", - async () => { - const key = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await key.commit(); - expectErrorMessage(async () => { - const returnedKey = await adapter.updateKeyPassword( - INVALID_INPUT, - key.value.hashed_password - ); - if (returnedKey === undefined) { - throw new LuciaError("AUTH_INVALID_KEY_ID"); - } - }, "AUTH_INVALID_KEY_ID"); - await clearAll(); - } - ); - await test("deleteNonPrimaryKey()", "Delete target key", async () => { - const key1 = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await key1.commit(); - const key2 = database.user().key({ - primary: false, - passwordDefined: true, - oneTime: false - }); - await key2.commit(); - await adapter.deleteNonPrimaryKey(key1.value.id); - await key1.notExits(); - await key2.exists(); - await clearAll(); - }); - await test( - "deleteNonPrimaryKey()", - "Avoid deleting primary key", - async () => { - const key = database.user().key({ - primary: true, - passwordDefined: true, - oneTime: false - }); - await key.commit(); - await adapter.deleteNonPrimaryKey(key.value.id); - await key.exists(); - await clearAll(); - } - ); - await test("deleteKeysByUserId()", "Delete keys of target user", async () => { - const key1 = database.user().key({ - primary: false, - passwordDefined: false, - oneTime: false - }); - const key2 = database.user().key({ - primary: false, - passwordDefined: false, - oneTime: false - }); - await key1.commit(); - await key2.commit(); - await adapter.deleteKeysByUserId(key1.value.user_id); - await key1.notExits(); - await key2.exists(); - await clearAll(); - }); - await test("deleteKeysByUserId()", "Delete primary keys", async () => { - const key = database.user().key({ - primary: false, - passwordDefined: false, - oneTime: false - }); - await key.commit(); - await adapter.deleteKeysByUserId(key.value.user_id); - await key.notExits(); - await clearAll(); - }); - await clearAll(); - if (endProcess) { - end(); - } -}; diff --git a/packages/adapter-test/src/validate.ts b/packages/adapter-test/src/validate.ts deleted file mode 100644 index 2a20306a5..000000000 --- a/packages/adapter-test/src/validate.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { LuciaErrorConstructor } from "lucia-auth"; - -export const isNull = (data: any) => { - if (data === null) return; - typeError(data, "null"); -}; -export const isEmptyArray = (data: unknown) => { - if (Array.isArray(data) && data.length === 0) return; - typeError(data, "array"); -}; - -export const expectErrorMessage = async ( - test: () => Promise | void, - expectedValue: ConstructorParameters[0] -) => { - try { - await test(); - throw new Error("No error was thrown"); - } catch (e) { - if (typeof e !== "object" || e === null) throw typeError(e, "object"); - if ("message" in e && e.message === expectedValue) return; - throw valueError( - "message" in e ? e.message : undefined, - expectedValue, - "Error message did not match" - ); - } -}; - -export const expectError = async (test: () => Promise | void) => { - try { - await test(); - throw new Error("No error was thrown"); - } catch (e) { - // expect error - } -}; - -export const typeError = (received: any, expected: string) => { - logErrorResult(received, `type ${expected}`); - return new Error("Target was not of expected type"); -}; - -export const valueError = ( - received: any, - expected: any, - errorMessage: string -) => { - logErrorResult(received, expected); - return new Error(errorMessage); -}; - -const logErrorResult = (received: any, expected: any) => { - console.log("received: "); - console.dir(received, { depth: null }); - console.log("expected: "); - console.dir(expected, { depth: null }); -}; diff --git a/packages/integration-oauth/.npmignore b/packages/integration-oauth/.npmignore deleted file mode 100644 index b512c09d4..000000000 --- a/packages/integration-oauth/.npmignore +++ /dev/null @@ -1 +0,0 @@ -node_modules \ No newline at end of file diff --git a/packages/integration-oauth/.npmrc b/packages/integration-oauth/.npmrc deleted file mode 100644 index b6f27f135..000000000 --- a/packages/integration-oauth/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/packages/integration-oauth/package.json b/packages/integration-oauth/package.json deleted file mode 100644 index b82e9768c..000000000 --- a/packages/integration-oauth/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@lucia-auth/oauth", - "version": "1.2.1", - "description": "OAuth integration for Lucia", - "main": "index.js", - "types": "index.d.ts", - "module": "index.js", - "type": "module", - "files": [ - "**/*" - ], - "scripts": { - "build": "shx rm -rf ./dist/* && tsc && shx cp ./package.json ./dist && shx cp ./README.md ./dist && shx cp .npmignore dist", - "auri.publish": "pnpm build && cd dist && pnpm install --no-frozen-lockfile && pnpm publish --no-git-checks --access public && cd ../" - }, - "keywords": [ - "lucia", - "lucia-auth", - "authentication", - "auth", - "oauth" - ], - "repository": { - "type": "git", - "url": "https://github.com/pilcrowOnPaper/lucia", - "directory": "packages/lucia" - }, - "author": "pilcrowonpaper", - "license": "MIT", - "exports": { - "./package.json": "./package.json", - ".": "./index.js", - "./providers": "./providers/index.js" - }, - "typesVersions": { - "*": { - "providers": [ - "providers/index.d.ts" - ] - } - }, - "devDependencies": { - "lucia-auth": "workspace:*" - }, - "peerDependencies": { - "lucia-auth": "1.x" - } -} diff --git a/packages/integration-oauth/src/ambient.d.ts b/packages/integration-oauth/src/ambient.d.ts deleted file mode 100644 index 31f8a1b3a..000000000 --- a/packages/integration-oauth/src/ambient.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// -declare namespace Lucia { - export type UserAttributes = { - username?: string; - }; - export type Auth = import("lucia-auth").Auth; -} diff --git a/packages/integration-oauth/src/index.ts b/packages/integration-oauth/src/index.ts deleted file mode 100644 index 06abefae1..000000000 --- a/packages/integration-oauth/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { provider, generateState, LuciaOAuthRequestError } from "./core.js"; - -export type { OAuthProvider } from "./core.js"; diff --git a/packages/integration-tokens/.eslintignore b/packages/integration-tokens/.eslintignore deleted file mode 100644 index 38972655f..000000000 --- a/packages/integration-tokens/.eslintignore +++ /dev/null @@ -1,13 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example - -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml -package-lock.json -yarn.lock diff --git a/packages/integration-tokens/.npmignore b/packages/integration-tokens/.npmignore deleted file mode 100644 index b512c09d4..000000000 --- a/packages/integration-tokens/.npmignore +++ /dev/null @@ -1 +0,0 @@ -node_modules \ No newline at end of file diff --git a/packages/integration-tokens/.npmrc b/packages/integration-tokens/.npmrc deleted file mode 100644 index b6f27f135..000000000 --- a/packages/integration-tokens/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/packages/integration-tokens/CHANGELOG.md b/packages/integration-tokens/CHANGELOG.md deleted file mode 100644 index 993ca60a4..000000000 --- a/packages/integration-tokens/CHANGELOG.md +++ /dev/null @@ -1,39 +0,0 @@ -# @lucia-auth/tokens - -## 1.0.2 - -### Patch changes - -- [#626](https://github.com/pilcrowOnPaper/lucia/pull/626) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : Update types - -## 1.0.1 - -### Patch changes - -- [#505](https://github.com/pilcrowOnPaper/lucia/pull/505) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : Remove `nanoid` dependency - -## 1.0.0 - -### Major changes - -- [#443](https://github.com/pilcrowOnPaper/lucia/pull/443) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : Release version 1.0! - -## 0.3.0 - -### Minor changes - -- [#430](https://github.com/pilcrowOnPaper/lucia/pull/430) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : [Breaking] Rename `timeout` option to `expiresIn` - -## 0.2.0 - -### Minor changes - -- [#424](https://github.com/pilcrowOnPaper/lucia/pull/424) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : [Breaking] Renamed `TokenError` to `LuciaTokenError` - -- [#424](https://github.com/pilcrowOnPaper/lucia/pull/424) by [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : [Breaking] Update `idToken()` and `passwordToken()` - -## 0.1.1 - -### Patch changes - -- By [@pilcrowOnPaper](https://github.com/pilcrowOnPaper) : republish diff --git a/packages/integration-tokens/README.md b/packages/integration-tokens/README.md deleted file mode 100644 index cff2618b0..000000000 --- a/packages/integration-tokens/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# @lucia-auth/oauth - -OAuth integration for Lucia. - -**[Documentation](https://lucia-auth.com/tokens/start-here/introduction)** - -**[Lucia documentation](https://lucia-auth.com)** - -**[Changelog](https://github.com/pilcrowOnPaper/lucia/blob/main/packages/integartion-tokens/CHANGELOG.md)** - -## Installation - -```bash -npm i @lucia-auth/tokens -pnpm add @lucia-auth/tokens -yarn add @lucia-auth/tokens -``` - -## Lucia version compatibility - -| Oauth package version | Lucia version | -| --------------------- | ------------- | -| 0.1.x | 0.9.x | diff --git a/packages/integration-tokens/package.json b/packages/integration-tokens/package.json deleted file mode 100644 index 662144e1b..000000000 --- a/packages/integration-tokens/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@lucia-auth/tokens", - "version": "1.0.2", - "description": "Tokens integration for Lucia", - "main": "index.js", - "types": "index.d.ts", - "module": "index.js", - "svelte": "index.js", - "type": "module", - "files": [ - "**/*" - ], - "scripts": { - "build": "shx rm -rf ./dist/* && tsc && shx cp ./package.json ./dist && shx cp ./README.md ./dist && shx cp .npmignore dist", - "auri.publish": "pnpm build && cd dist && pnpm install --no-frozen-lockfile && pnpm publish --no-git-checks --access public && cd ../" - }, - "keywords": [ - "lucia", - "lucia-auth", - "authentication", - "auth", - "tokens" - ], - "exports": { - "./package.json": "./package.json", - ".": "./index.js" - }, - "repository": { - "type": "git", - "url": "https://github.com/pilcrowOnPaper/lucia", - "directory": "packages/lucia-auth" - }, - "author": "pilcrowonpaper", - "license": "MIT", - "dependencies": { - "lucia-auth": "workspace:*" - }, - "peerDependencies": { - "lucia-auth": "1.x" - } -} diff --git a/packages/integration-tokens/src/ambient.d.ts b/packages/integration-tokens/src/ambient.d.ts deleted file mode 100644 index 31f8a1b3a..000000000 --- a/packages/integration-tokens/src/ambient.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// -declare namespace Lucia { - export type UserAttributes = { - username?: string; - }; - export type Auth = import("lucia-auth").Auth; -} diff --git a/packages/integration-tokens/src/error.ts b/packages/integration-tokens/src/error.ts deleted file mode 100644 index 5213d05b7..000000000 --- a/packages/integration-tokens/src/error.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class LuciaTokenError extends Error { - public message: ErrorMessage; - constructor(errorMessage: ErrorMessage) { - super(errorMessage); - this.message = errorMessage; - } -} - -type ErrorMessage = - | "INVALID_TOKEN" - | "EXPIRED_TOKEN" - | "INVALID_USER_ID" - | "DUPLICATE_TOKEN"; diff --git a/packages/integration-tokens/src/index.ts b/packages/integration-tokens/src/index.ts deleted file mode 100644 index 8bdef5e0b..000000000 --- a/packages/integration-tokens/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { idToken, passwordToken, Token } from "./tokens.js"; -export { LuciaTokenError } from "./error.js"; diff --git a/packages/integration-tokens/src/tokens.ts b/packages/integration-tokens/src/tokens.ts deleted file mode 100644 index 89e7b532d..000000000 --- a/packages/integration-tokens/src/tokens.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { LuciaTokenError } from "./error.js"; -import { generateRandomString } from "./utils/nanoid.js"; - -import type { Auth, SingleUseKey, LuciaError } from "lucia-auth"; - -export class Token { - private readonly value: string; - - public readonly toString = () => this.value; - public readonly expiresAt: Date; - public readonly expired: boolean; - public readonly userId: string; - public readonly key: Readonly; - - constructor(value: string, key: SingleUseKey) { - this.value = value; - this.expiresAt = key.expiresAt; - this.expired = key.expired; - this.userId = key.userId; - this.key = key; - } -} - -type TokenWrapper = Readonly<{ - issue: (...args: any) => Promise; - validate: (...args: any) => Promise; - getUserTokens: (userId: string) => Promise; - invalidateAllUserTokens: (userId: string) => Promise; - invalidate?: (token: string) => Promise; -}>; - -export const idToken = ( - auth: Auth, - name: string, - options: { - expiresIn: number; - length?: number; - generate?: (length?: number) => string; - } -) => { - return { - issue: async (userId: string) => { - const generate = options.generate ?? generateRandomString; - const token = generate(options.length ?? 43); - try { - const key = await auth.createKey(userId, { - type: "single_use", - providerId: name, - providerUserId: token, - password: null, - expiresIn: options.expiresIn - }); - return new Token(token, key); - } catch (e) { - const error = e as Partial; - if (error.message === "AUTH_INVALID_USER_ID") - throw new LuciaTokenError("INVALID_USER_ID"); - if (error.message === "AUTH_DUPLICATE_KEY_ID") - throw new LuciaTokenError("DUPLICATE_TOKEN"); - throw e; - } - }, - validate: async (token: string) => { - try { - const key = await auth.useKey(name, token, null); - if (key.type !== "single_use") - throw new LuciaTokenError("INVALID_TOKEN"); - return new Token(token, key); - } catch (e) { - const error = e as Partial; - if (error.message === "AUTH_INVALID_KEY_ID") - throw new LuciaTokenError("INVALID_TOKEN"); - if (error.message === "AUTH_EXPIRED_KEY") - throw new LuciaTokenError("EXPIRED_TOKEN"); - throw e; - } - }, - getUserTokens: async (userId: string) => { - const keys = await auth.getAllUserKeys(userId); - const targetKeys = keys.filter((key): key is SingleUseKey => { - return key.type === "single_use" && key.providerId === name; - }); - return targetKeys.map((key) => { - return new Token(key.providerUserId, key); - }); - }, - invalidate: async (token: string) => { - await auth.deleteKey(name, token); - }, - invalidateAllUserTokens: async (userId: string) => { - try { - const keys = await auth.getAllUserKeys(userId); - const targetKeys = keys.filter((key) => key.providerId === name); - await Promise.all( - targetKeys.map((key) => - auth.deleteKey(key.providerId, key.providerUserId) - ) - ); - } catch (e) { - const error = e as Partial; - // ignore invalid user id to be consistent with similar Lucia APIs - if (error.message === "AUTH_INVALID_USER_ID") return; - throw e; - } - } - } as const satisfies TokenWrapper; -}; - -export const passwordToken = ( - auth: Auth, - name: string, - options: { - expiresIn: number; - length?: number; - generate?: (length?: number) => string; - } -) => { - const defaultGenerateRandomPassword = (length: number) => { - return generateRandomString(length, "0123456789"); - }; - return { - issue: async (userId: string) => { - const generate = options.generate ?? defaultGenerateRandomPassword; - const token = generate(options.length ?? 8); - const providerUserId = [userId, token].join("."); - try { - const key = await auth.createKey(userId, { - type: "single_use", - providerId: name, - providerUserId: providerUserId, - password: null, - expiresIn: options.expiresIn - }); - return new Token(token, key); - } catch (e) { - const error = e as Partial; - if (error.message === "AUTH_INVALID_USER_ID") - throw new LuciaTokenError("INVALID_USER_ID"); - if (error.message === "AUTH_DUPLICATE_KEY_ID") - throw new LuciaTokenError("DUPLICATE_TOKEN"); - throw e; - } - }, - validate: async (token: string, userId: string) => { - const providerUserId = [userId, token].join("."); - try { - const key = await auth.useKey(name, providerUserId, null); - if (key.type !== "single_use") - throw new LuciaTokenError("INVALID_TOKEN"); - return new Token(token, key); - } catch (e) { - const error = e as Partial; - if (error.message === "AUTH_INVALID_KEY_ID") - throw new LuciaTokenError("INVALID_TOKEN"); - if (error.message === "AUTH_EXPIRED_KEY") - throw new LuciaTokenError("EXPIRED_TOKEN"); - throw e; - } - }, - getUserTokens: async (userId: string) => { - try { - const keys = await auth.getAllUserKeys(userId); - const tokenKeys = keys.filter((key): key is SingleUseKey => { - if (!key.providerUserId.includes(".")) return false; - return key.type === "single_use" && key.providerId === name; - }); - return tokenKeys.map((key) => { - const [_userId, token] = key.providerUserId.split("."); - return new Token(token, key); - }); - } catch (e) { - const error = e as Partial; - if (error.message === "AUTH_INVALID_USER_ID") - throw new LuciaTokenError("INVALID_USER_ID"); - throw e; - } - }, - invalidateAllUserTokens: async (userId: string) => { - try { - const keys = await auth.getAllUserKeys(userId); - const targetKeys = keys.filter((key) => key.providerId === name); - await Promise.all( - targetKeys.map((key) => - auth.deleteKey(key.providerId, key.providerUserId) - ) - ); - } catch (e) { - const error = e as Partial; - // ignore invalid user id to be consistent with similar Lucia APIs - if (error.message === "AUTH_INVALID_USER_ID") return; - throw e; - } - } - } as const satisfies TokenWrapper; -}; diff --git a/packages/integration-tokens/src/utils/nanoid.ts b/packages/integration-tokens/src/utils/nanoid.ts deleted file mode 100644 index b4294affa..000000000 --- a/packages/integration-tokens/src/utils/nanoid.ts +++ /dev/null @@ -1,33 +0,0 @@ -// code copied from Nanoid: -// https://github.com/ai/nanoid/blob/9b748729f8ad5409503b508b65958636e55bd87a/index.browser.js -// nanoid uses Node dependencies on default bundler settings - -// TODO: on next major update, use generateRandomString imported from lucia-auth - -const getRandomValues = (bytes: number) => - crypto.getRandomValues(new Uint8Array(bytes)); - -const DEFAULT_ALPHABET = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; - -export const generateRandomString = ( - size: number, - alphabet = DEFAULT_ALPHABET -) => { - const mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1; - const step = -~((1.6 * mask * size) / alphabet.length); - - let bytes = getRandomValues(step); - let id = ""; - let index = 0; - - while (id.length !== size) { - id += alphabet[bytes[index] & mask] ?? ""; - index += 1; - if (index > bytes.length) { - bytes = getRandomValues(step); - index = 0; - } - } - return id; -}; diff --git a/packages/integration-tokens/tsconfig.json b/packages/integration-tokens/tsconfig.json deleted file mode 100644 index 0bf7d70b9..000000000 --- a/packages/integration-tokens/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "noImplicitAny": true, - "module": "es2022", - "moduleResolution": "nodenext", - "target": "es2022", - "allowSyntheticDefaultImports": true, - "declaration": true, - "outDir": "./dist", - "strict": true - }, - "include": ["src"], - "exclude": ["node_modules/", "**/__tests__/*"] -} diff --git a/packages/lucia-auth/.gitignore b/packages/lucia-auth/.gitignore deleted file mode 100644 index 59462ab84..000000000 --- a/packages/lucia-auth/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/node_modules -/dist -.DS_Store -/.svelte-kit \ No newline at end of file diff --git a/packages/lucia-auth/.npmignore b/packages/lucia-auth/.npmignore deleted file mode 100644 index b512c09d4..000000000 --- a/packages/lucia-auth/.npmignore +++ /dev/null @@ -1 +0,0 @@ -node_modules \ No newline at end of file diff --git a/packages/lucia-auth/.prettierignore b/packages/lucia-auth/.prettierignore deleted file mode 100644 index 38972655f..000000000 --- a/packages/lucia-auth/.prettierignore +++ /dev/null @@ -1,13 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example - -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml -package-lock.json -yarn.lock diff --git a/packages/lucia-auth/package.json b/packages/lucia-auth/package.json deleted file mode 100644 index 1b3417c3f..000000000 --- a/packages/lucia-auth/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "lucia-auth", - "version": "1.8.0", - "description": "A simple and flexible authentication library", - "main": "index.js", - "types": "index.d.ts", - "module": "index.js", - "svelte": "index.js", - "type": "module", - "files": [ - "**/*" - ], - "scripts": { - "build": "shx rm -rf ./dist/* && tsc && shx cp ./package.json ./dist && shx cp ./README.md ./dist && shx cp .npmignore dist", - "auri.publish": "pnpm build && cd dist && pnpm install --no-frozen-lockfile && pnpm publish --no-git-checks --access public && cd ../", - "test": "NODE_OPTIONS=--experimental-global-webcrypto vitest run", - "test.debug": "vitest run src/utils/debug.test.ts" - }, - "keywords": [ - "lucia", - "lucia-auth", - "authentication", - "auth" - ], - "exports": { - "./package.json": "./package.json", - ".": "./index.js", - "./middleware": "./middleware/index.js", - "./polyfill/node": "./polyfill/node.js" - }, - "typesVersions": { - "*": { - "middleware": [ - "middleware/index.d.ts" - ], - "polyfill/node": [ - "polyfill/node.d.ts" - ] - } - }, - "repository": { - "type": "git", - "url": "https://github.com/pilcrowOnPaper/lucia", - "directory": "packages/lucia-auth" - }, - "author": "pilcrowonpaper", - "license": "MIT", - "devDependencies": { - "@sveltejs/kit": "1.10.0", - "@types/express": "^4.17.17", - "@types/node": "^18.6.2", - "prettier": "^2.3.0", - "vitest": "^0.30.1" - } -} diff --git a/packages/lucia-auth/src/auth/cookie.ts b/packages/lucia-auth/src/auth/cookie.ts deleted file mode 100644 index 8d477828e..000000000 --- a/packages/lucia-auth/src/auth/cookie.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Env, Session } from "./index.js"; -import { type CookieAttributes, serializeCookie } from "../utils/cookie.js"; - -export const SESSION_COOKIE_NAME = "auth_session"; - -export type CookieOption = { - sameSite?: "strict" | "lax"; - path?: string; - domain?: string; -}; - -export const createSessionCookie = ( - session: Session | null, - env: Env, - options: CookieOption -) => { - return new Cookie(SESSION_COOKIE_NAME, session?.sessionId ?? "", { - ...options, - httpOnly: true, - expires: new Date(session?.idlePeriodExpiresAt ?? 0), - secure: env === "PROD" - }); -}; - -export class Cookie { - constructor(name: string, value: string, options: CookieAttributes) { - this.name = name; - this.value = value; - this.attributes = options; - } - public readonly name: string; - public readonly value: string; - public readonly attributes: CookieAttributes; - public readonly serialize = () => { - return serializeCookie(this.name, this.value, this.attributes); - }; -} diff --git a/packages/lucia-auth/src/auth/index.ts b/packages/lucia-auth/src/auth/index.ts deleted file mode 100644 index a8da9fb9b..000000000 --- a/packages/lucia-auth/src/auth/index.ts +++ /dev/null @@ -1,729 +0,0 @@ -import { - Cookie, - CookieOption, - SESSION_COOKIE_NAME, - createSessionCookie -} from "./cookie.js"; -import { logError } from "../utils/log.js"; -import { generateScryptHash, validateScryptHash } from "../utils/crypto.js"; -import { generateRandomString } from "../utils/nanoid.js"; -import { LuciaError } from "./error.js"; -import { parseCookie } from "../utils/cookie.js"; -import { validateDatabaseSession } from "./session.js"; -import { transformDatabaseKey, getOneTimeKeyExpiration } from "./key.js"; -import { isWithinExpiration } from "../utils/date.js"; -import { AuthRequest } from "./request.js"; -import { lucia as defaultMiddleware } from "../middleware/index.js"; - -import type { UserSchema, SessionSchema, KeySchema } from "./schema.js"; -import type { Adapter, UserAdapter, SessionAdapter } from "./adapter.js"; -import type { LuciaErrorConstructor } from "./error.js"; -import type { Middleware, LuciaRequest } from "./request.js"; -import { debug } from "../utils/debug.js"; - -export type Session = Readonly<{ - sessionId: string; - userId: string; - activePeriodExpiresAt: Date; - idlePeriodExpiresAt: Date; - state: "idle" | "active"; - fresh: boolean; -}>; - -export type Key = SingleUseKey | PersistentKey; - -export type SingleUseKey = Readonly<{ - type: "single_use"; - userId: string; - providerId: string; - providerUserId: string; - passwordDefined: boolean; - expiresAt: Date; - expired: boolean; -}>; - -export type PersistentKey = Readonly<{ - type: "persistent"; - userId: string; - providerId: string; - providerUserId: string; - passwordDefined: boolean; - primary: boolean; -}>; - -export type Env = "DEV" | "PROD"; -export type User = ReturnType; - -export const lucia = (config: C) => { - return new Auth(config); -}; - -const validateConfiguration = (config: Configuration) => { - const adapterProvided = config.adapter; - if (!adapterProvided) { - logError('Adapter is not defined in configuration ("config.adapter")'); - process.exit(1); - } -}; - -export class Auth { - private adapter: Adapter; - private generateUserId: () => MaybePromise; - private sessionCookieOption: CookieOption; - private sessionExpiresIn: { - activePeriod: number; - idlePeriod: number; - }; - private env: Env; - private hash: { - generate: (s: string) => MaybePromise; - validate: (s: string, hash: string) => MaybePromise; - }; - private autoDatabaseCleanup: boolean; - protected middleware: C["middleware"] extends Middleware - ? C["middleware"] - : ReturnType; - private csrfProtection: boolean; - private origin: string[]; - private experimental: { - debugMode: boolean; - }; - - constructor(config: C) { - validateConfiguration(config); - const defaultSessionCookieOption: CookieOption = { - sameSite: "lax", - path: "/" - }; - if ("user" in config.adapter) { - let userAdapter = config.adapter.user(LuciaError); - let sessionAdapter = config.adapter.session(LuciaError); - if ("getSessionAndUserBySessionId" in userAdapter) { - const { getSessionAndUserBySessionId: _, ...extractedUserAdapter } = - userAdapter; - userAdapter = extractedUserAdapter; - } - if ("getSessionAndUserBySessionId" in sessionAdapter) { - const { getSessionAndUserBySessionId: _, ...extractedSessionAdapter } = - sessionAdapter; - sessionAdapter = extractedSessionAdapter; - } - this.adapter = { - ...userAdapter, - ...sessionAdapter - }; - } else { - this.adapter = config.adapter(LuciaError); - } - this.generateUserId = - config.generateCustomUserId ?? (() => generateRandomString(15)); - this.env = config.env; - this.csrfProtection = config.csrfProtection ?? true; - this.sessionExpiresIn = { - activePeriod: - config.sessionExpiresIn?.activePeriod ?? 1000 * 60 * 60 * 24, - idlePeriod: - config.sessionExpiresIn?.idlePeriod ?? 1000 * 60 * 60 * 24 * 14 - }; - this.autoDatabaseCleanup = config.autoDatabaseCleanup ?? true; - this._transformDatabaseUser = (databaseUser) => { - const defaultTransform = ({ id }: UserSchema) => { - return { - userId: id - } as const; - }; - const transform = config.transformDatabaseUser ?? defaultTransform; - return transform(databaseUser) as any; - }; - this.sessionCookieOption = - config.sessionCookie ?? defaultSessionCookieOption; - this.hash = { - generate: config.hash?.generate ?? generateScryptHash, - validate: config.hash?.validate ?? validateScryptHash - }; - this.middleware = config.middleware ?? defaultMiddleware(); - this.origin = config.origin ?? []; - this.experimental = { - debugMode: config.experimental?.debugMode ?? false - }; - debug.init(this.experimental.debugMode); - } - protected _transformDatabaseUser: ( - databaseUser: UserSchema - ) => C["transformDatabaseUser"] extends Function - ? ReturnType - : { userId: string }; - public transformDatabaseUser = (databaseUser: UserSchema): User => { - return this._transformDatabaseUser(databaseUser); - }; - public getUser = async (userId: string): Promise => { - const databaseUser = await this.adapter.getUser(userId); - if (!databaseUser) { - throw new LuciaError("AUTH_INVALID_USER_ID"); - } - const user = this.transformDatabaseUser(databaseUser); - return user; - }; - - public getSessionUser = async ( - sessionId: string - ): Promise<{ - user: User; - session: Session; - }> => { - if (sessionId.length !== 40) { - debug.session.fail("Expected id length to be 40", sessionId); - throw new LuciaError("AUTH_INVALID_SESSION_ID"); - } - let databaseUser: UserSchema | null; - let databaseSession: SessionSchema | null; - if (this.adapter.getSessionAndUserBySessionId !== undefined) { - const databaseUserSession = - await this.adapter.getSessionAndUserBySessionId(sessionId); - if (!databaseUserSession) { - debug.session.fail("Session not found", sessionId); - throw new LuciaError("AUTH_INVALID_SESSION_ID"); - } - databaseUser = databaseUserSession.user; - databaseSession = databaseUserSession.session; - } else { - databaseSession = await this.adapter.getSession(sessionId); - if (!databaseSession) { - debug.session.fail("Session not found", sessionId); - throw new LuciaError("AUTH_INVALID_SESSION_ID"); - } - databaseUser = await this.adapter.getUser(databaseSession.user_id); - if (!databaseUser) { - debug.session.fail("User not found", databaseSession.user_id); - throw new LuciaError("AUTH_INVALID_SESSION_ID"); - } - } - const session = validateDatabaseSession(databaseSession); - if (!session) { - debug.session.fail( - `Session expired at ${new Date(Number(databaseSession.idle_expires))}`, - sessionId - ); - if (this.autoDatabaseCleanup) { - await this.adapter.deleteSession(sessionId); - } - throw new LuciaError("AUTH_INVALID_SESSION_ID"); - } - return { - user: this.transformDatabaseUser(databaseUser), - session - }; - }; - - public createUser = async (data: { - primaryKey: { - providerId: string; - providerUserId: string; - password: string | null; - } | null; - attributes: Lucia.UserAttributes; - }): Promise => { - const userId = await this.generateUserId(); - const userAttributes = data.attributes ?? {}; - if (data.primaryKey === null) { - const databaseUser = await this.adapter.setUser( - userId, - userAttributes, - null - ); - if (databaseUser) return this.transformDatabaseUser(databaseUser); - return await this.getUser(userId); - } - const keyId = `${data.primaryKey.providerId}:${data.primaryKey.providerUserId}`; - const password = data.primaryKey.password; - const hashedPassword = password ? await this.hash.generate(password) : null; - const databaseUser = await this.adapter.setUser(userId, userAttributes, { - id: keyId, - user_id: userId, - hashed_password: hashedPassword, - primary_key: true, - expires: null - }); - if (databaseUser) return this.transformDatabaseUser(databaseUser); - return await this.getUser(userId); - }; - - public updateUserAttributes = async ( - userId: string, - attributes: Partial - ): Promise => { - const [updatedDatabaseUser] = await Promise.all([ - this.adapter.updateUserAttributes(userId, attributes), - this.autoDatabaseCleanup - ? await this.deleteDeadUserSessions(userId) - : null - ]); - if (updatedDatabaseUser) - return this.transformDatabaseUser(updatedDatabaseUser); - return await this.getUser(userId); - }; - - public deleteUser = async (userId: string): Promise => { - await this.adapter.deleteSessionsByUserId(userId); - await this.adapter.deleteKeysByUserId(userId); - await this.adapter.deleteUser(userId); - }; - - public useKey = async ( - providerId: string, - providerUserId: string, - password: string | null - ): Promise => { - const keyId = `${providerId}:${providerUserId}`; - - // TODO: remove check in v2 - const shouldDataBeDeleted = async (data: KeySchema) => { - const persistentKey = data.expires === null; - if (persistentKey) return false; - - if (data.hashed_password === null) return true; - if (password === null) return false; - - return await this.hash.validate(password, data.hashed_password); - }; - - const databaseKeyData = await this.adapter.getKey( - keyId, - shouldDataBeDeleted - ); - if (!databaseKeyData) { - debug.key.fail("Key not found", keyId); - throw new LuciaError("AUTH_INVALID_KEY_ID"); - } - try { - const singleUse = !!databaseKeyData.expires; - const hashedPassword = databaseKeyData.hashed_password; - if (hashedPassword) { - debug.key.info("Key includes password"); - if (!password) { - debug.key.fail("Key password not provided", keyId); - throw new LuciaError("AUTH_INVALID_PASSWORD"); - } - if (hashedPassword.startsWith("$2a")) { - throw new LuciaError("AUTH_OUTDATED_PASSWORD"); - } - const validPassword = await this.hash.validate( - password, - hashedPassword - ); - if (!validPassword) { - debug.key.fail("Incorrect key password", password); - throw new LuciaError("AUTH_INVALID_PASSWORD"); - } - debug.key.notice("Validated key password"); - } else { - debug.key.info("No password included in key"); - } - if (singleUse) { - debug.key.info("Key type: single-use"); - const withinExpiration = isWithinExpiration(databaseKeyData.expires); - if (!withinExpiration) { - debug.key.fail( - `Key expired at ${new Date( - databaseKeyData.expires - ).toLocaleDateString()}`, - keyId - ); - throw new LuciaError("AUTH_EXPIRED_KEY"); - } - await this.adapter.deleteNonPrimaryKey(databaseKeyData.id); - } else { - debug.key.info("Key type: persistent"); - } - debug.key.success("Validated key", keyId); - const key = transformDatabaseKey(databaseKeyData); - return key; - } catch (e) { - if (e instanceof LuciaError && e.message === "AUTH_EXPIRED_KEY") { - await this.adapter.deleteNonPrimaryKey(databaseKeyData.id); - } - throw e; - } - }; - - public getSession = async (sessionId: string): Promise => { - if (sessionId.length !== 40) { - debug.session.fail("Expected id length to be 40", sessionId); - throw new LuciaError("AUTH_INVALID_SESSION_ID"); - } - const databaseSession = await this.adapter.getSession(sessionId); - if (!databaseSession) { - debug.session.fail("Session not found", sessionId); - throw new LuciaError("AUTH_INVALID_SESSION_ID"); - } - const session = validateDatabaseSession(databaseSession); - if (!session) { - debug.session.fail( - `Session expired at ${new Date(Number(databaseSession.idle_expires))}`, - sessionId - ); - if (this.autoDatabaseCleanup) { - await this.adapter.deleteSession(sessionId); - } - throw new LuciaError("AUTH_INVALID_SESSION_ID"); - } - return session; - }; - - public getAllUserSessions = async (userId: string): Promise => { - // validate user id - await this.getUser(userId); - const databaseData = await this.adapter.getSessionsByUserId(userId); - const validStoredUserSessions = databaseData - .map((databaseSession) => { - return validateDatabaseSession(databaseSession); - }) - .filter((session): session is Session => session !== null); - const deadStoredUserSessionIds = databaseData - .map((databaseSession) => { - return databaseSession.id; - }) - .filter((sessionId) => { - return !validStoredUserSessions.some( - (validSession) => validSession.sessionId === sessionId - ); - }); - if (deadStoredUserSessionIds.length > 0) { - await Promise.all( - deadStoredUserSessionIds.map((deadSessionId) => - this.adapter.deleteSession(deadSessionId) - ) - ); - } - return validStoredUserSessions; - }; - - public validateSession = async (sessionId: string): Promise => { - const session = await this.getSession(sessionId); - if (session.state === "active") { - debug.session.success("Validated session", session.sessionId); - return session; - } - const renewedSession = await this.renewSession(sessionId); - return renewedSession; - }; - - public validateSessionUser = async ( - sessionId: string - ): Promise<{ session: Session; user: User }> => { - const { session, user } = await this.getSessionUser(sessionId); - if (session.state === "active") { - debug.session.success("Validated session", session.sessionId); - return { session, user }; - } - const renewedSession = await this.renewSession(sessionId); - return { - session: renewedSession, - user - }; - }; - - public generateSessionId = (): readonly [ - sessionId: string, - activePeriodExpiresAt: Date, - idlePeriodExpiresAt: Date - ] => { - const sessionId = generateRandomString(40); - const activePeriodExpiresAt = new Date( - new Date().getTime() + this.sessionExpiresIn.activePeriod - ); - const idlePeriodExpiresAt = new Date( - activePeriodExpiresAt.getTime() + this.sessionExpiresIn.idlePeriod - ); - return [sessionId, activePeriodExpiresAt, idlePeriodExpiresAt]; - }; - - public createSession = async (userId: string): Promise => { - const [sessionId, activePeriodExpiresAt, idlePeriodExpiresAt] = - this.generateSessionId(); - await Promise.all([ - this.adapter.setSession({ - id: sessionId, - user_id: userId, - active_expires: activePeriodExpiresAt.getTime(), - idle_expires: idlePeriodExpiresAt.getTime() - }), - this.autoDatabaseCleanup - ? await this.deleteDeadUserSessions(userId) - : null - ]); - return { - userId, - activePeriodExpiresAt, - sessionId, - idlePeriodExpiresAt, - state: "active", - fresh: true - }; - }; - - public renewSession = async (sessionId: string): Promise => { - if (sessionId.length !== 40) { - debug.session.fail("Expected id length to be 40", sessionId); - throw new LuciaError("AUTH_INVALID_SESSION_ID"); - } - const databaseSession = await this.adapter.getSession(sessionId); - if (!databaseSession) { - debug.session.fail("Session not found", sessionId); - throw new LuciaError("AUTH_INVALID_SESSION_ID"); - } - const session = validateDatabaseSession(databaseSession); - if (!session) { - debug.session.fail( - `Session expired at ${new Date( - Number(databaseSession.idle_expires) - ).toLocaleDateString()}`, - sessionId - ); - if (this.autoDatabaseCleanup) { - await this.adapter.deleteSession(sessionId); - } - throw new LuciaError("AUTH_INVALID_SESSION_ID"); - } - const [renewedSession] = await Promise.all([ - await this.createSession(session.userId), - this.autoDatabaseCleanup - ? await this.deleteDeadUserSessions(session.userId) - : null - ]); - debug.session.success("Session renewed", renewedSession.sessionId); - return renewedSession; - }; - - public invalidateSession = async (sessionId: string): Promise => { - await this.adapter.deleteSession(sessionId); - debug.session.notice("Invalidated session", sessionId); - }; - - public invalidateAllUserSessions = async (userId: string): Promise => { - await this.adapter.deleteSessionsByUserId(userId); - }; - - public deleteDeadUserSessions = async (userId: string): Promise => { - const databaseSessions = await this.adapter.getSessionsByUserId(userId); - const deadSessionIds = databaseSessions - .filter((databaseSession) => { - return validateDatabaseSession(databaseSession) === null; - }) - .map((databaseSession) => databaseSession.id); - if (deadSessionIds.length === 0) return; - await Promise.all( - deadSessionIds.map((deadSessionId) => { - this.adapter.deleteSession(deadSessionId); - }) - ); - }; - - public parseRequestHeaders = (request: LuciaRequest): string | null => { - debug.request.init(request.method ?? "", request.url ?? ""); - if (request.method === null) { - debug.request.fail("Request method unavailable"); - throw new LuciaError("AUTH_INVALID_REQUEST"); - } - if (request.url === null) { - debug.request.fail("Request url unavailable"); - throw new LuciaError("AUTH_INVALID_REQUEST"); - } - const cookies = parseCookie(request.headers.cookie ?? ""); - const sessionId = cookies[SESSION_COOKIE_NAME] ?? null; - if (sessionId) { - debug.request.info("Found session cookie", sessionId); - } else { - debug.request.info("No session cookie found"); - } - const csrfCheck = - request.method.toUpperCase() !== "GET" && - request.method.toUpperCase() !== "HEAD"; - if (csrfCheck && this.csrfProtection) { - const requestOrigin = request.headers.origin; - if (!requestOrigin) { - debug.request.fail("No request origin available"); - throw new LuciaError("AUTH_INVALID_REQUEST"); - } - try { - const url = new URL(request.url); - if (![url.origin, ...this.origin].includes(requestOrigin)) { - debug.request.fail("Invalid request origin", requestOrigin); - throw new LuciaError("AUTH_INVALID_REQUEST"); - } - debug.request.info("Valid request origin", requestOrigin); - } catch { - debug.request.fail("Invalid origin string", requestOrigin); - // failed to parse url - throw new LuciaError("AUTH_INVALID_REQUEST"); - } - } else { - debug.request.notice("Skipping CSRF check"); - } - return sessionId; - }; - - public handleRequest = ( - // cant reference middleware type with Lucia.Auth - ...args: Auth["middleware"] extends Middleware ? Args : never - ): AuthRequest => { - const middleware = this.middleware as Middleware; - return new AuthRequest(this, middleware(...[...args, this.env])); - }; - - public createSessionCookie = (session: Session | null): Cookie => { - return createSessionCookie(session, this.env, this.sessionCookieOption); - }; - - public createKey = async < - KeyData extends - | { - readonly type: PersistentKey["type"]; - providerId: string; - providerUserId: string; - password: string | null; - } - | { - readonly type: SingleUseKey["type"]; - providerId: string; - providerUserId: string; - password: string | null; - expiresIn: number; - } - >( - userId: string, - keyData: KeyData - ): Promise> => { - const keyId = `${keyData.providerId}:${keyData.providerUserId}`; - let hashedPassword: string | null = null; - if (keyData.password !== null) { - hashedPassword = await this.hash.generate(keyData.password); - } - if (keyData.type === "persistent") { - await this.adapter.setKey({ - id: keyId, - user_id: userId, - hashed_password: hashedPassword, - primary_key: false, - expires: null - }); - return { - type: "persistent", - providerId: keyData.providerId, - providerUserId: keyData.providerUserId, - primary: false, - passwordDefined: !!keyData.password, - userId - } satisfies PersistentKey as any; - } - const expiresAt = getOneTimeKeyExpiration(keyData.expiresIn); - if (expiresAt == null) { - throw new LuciaError("UNKNOWN_ERROR"); - } - await this.adapter.setKey({ - id: keyId, - user_id: userId, - hashed_password: hashedPassword, - primary_key: false, - expires: expiresAt.getTime() - }); - return { - type: "single_use", - providerId: keyData.providerId, - providerUserId: keyData.providerUserId, - userId, - expiresAt, - expired: !isWithinExpiration(keyData.expiresIn), - passwordDefined: !!keyData.password - } satisfies SingleUseKey as any; - }; - - public deleteKey = async ( - providerId: string, - providerUserId: string - ): Promise => { - const keyId = `${providerId}:${providerUserId}`; - await this.adapter.deleteNonPrimaryKey(keyId); - }; - - public getKey = async ( - providerId: string, - providerUserId: string - ): Promise => { - const keyId = `${providerId}:${providerUserId}`; - const shouldDataBeDeleted = async () => false; - const databaseKey = await this.adapter.getKey(keyId, shouldDataBeDeleted); - if (!databaseKey) { - throw new LuciaError("AUTH_INVALID_KEY_ID"); - } - const key = transformDatabaseKey(databaseKey); - return key; - }; - - public getAllUserKeys = async (userId: string): Promise => { - // validate user id - await this.getUser(userId); - const databaseKeys = await this.adapter.getKeysByUserId(userId); - return databaseKeys.map((dbKey) => transformDatabaseKey(dbKey)); - }; - - public updateKeyPassword = async ( - providerId: string, - providerUserId: string, - password: string | null - ): Promise => { - const keyId = `${providerId}:${providerUserId}`; - let updatedDatabaseKey: KeySchema | void; - if (password === null) { - updatedDatabaseKey = await this.adapter.updateKeyPassword(keyId, null); - } else { - const hashedPassword = await this.hash.generate(password); - updatedDatabaseKey = await this.adapter.updateKeyPassword( - keyId, - hashedPassword - ); - } - if (updatedDatabaseKey) return; - // validate key - await this.getKey(providerId, providerUserId); - }; -} - -type MaybePromise = T | Promise; - -export type Configuration = { - adapter: - | ((E: LuciaErrorConstructor) => Adapter) - | { - user: (E: LuciaErrorConstructor) => UserAdapter | Adapter; - session: (E: LuciaErrorConstructor) => SessionAdapter | Adapter; - }; - env: Env; - - autoDatabaseCleanup?: boolean; - csrfProtection?: boolean; - generateCustomUserId?: () => MaybePromise; - hash?: { - generate: (s: string) => MaybePromise; - validate: (s: string, hash: string) => MaybePromise; - }; - middleware?: Middleware; - origin?: string[]; - sessionExpiresIn?: { - activePeriod: number; - idlePeriod: number; - }; - sessionCookie?: CookieOption; - transformDatabaseUser?: ( - databaseUser: Required - ) => Record; - experimental?: { - debugMode?: boolean; - }; -}; - -type GetKeyFromKeyType = Type extends PersistentKey["type"] - ? PersistentKey - : Type extends SingleUseKey["type"] - ? SingleUseKey - : never; diff --git a/packages/lucia-auth/src/auth/key.ts b/packages/lucia-auth/src/auth/key.ts deleted file mode 100644 index 5547b43c5..000000000 --- a/packages/lucia-auth/src/auth/key.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { KeySchema } from "./schema.js"; -import type { Key } from "./index.js"; -import { isWithinExpiration } from "../utils/date.js"; - -export const transformDatabaseKey = (databaseKey: KeySchema): Key => { - const [providerId, ...providerUserIdSegments] = databaseKey.id.split(":"); - const isPersistent = databaseKey.expires === null; - const providerUserId = providerUserIdSegments.join(":"); - const userId = databaseKey.user_id; - const isPasswordDefined = !!databaseKey.hashed_password; - if (isPersistent) { - return { - type: "persistent", - primary: databaseKey.primary_key, - providerId, - providerUserId, - userId, - passwordDefined: isPasswordDefined - }; - } - return { - type: "single_use", - providerId, - providerUserId, - userId, - expiresAt: new Date(databaseKey.expires), - expired: !isWithinExpiration(databaseKey.expires), - passwordDefined: isPasswordDefined - }; -}; - -export const getOneTimeKeyExpiration = ( - duration: number | null | undefined -): null | Date => { - if (typeof duration !== "number") return null; - return new Date(duration * 1000 + new Date().getTime()); -}; diff --git a/packages/lucia-auth/src/auth/schema.ts b/packages/lucia-auth/src/auth/schema.ts deleted file mode 100644 index 18a5f5197..000000000 --- a/packages/lucia-auth/src/auth/schema.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type KeySchema = Readonly<{ - id: string; - hashed_password: string | null; - primary_key: boolean; - user_id: string; - expires: number | null; -}>; - -export type UserSchema = Readonly< - { - id: string; - } & Required ->; - -export type SessionSchema = Readonly<{ - id: string; - active_expires: number | bigint; - idle_expires: number | bigint; - user_id: string; -}>; diff --git a/packages/lucia-auth/src/auth/session.test.ts b/packages/lucia-auth/src/auth/session.test.ts deleted file mode 100644 index c4018a97c..000000000 --- a/packages/lucia-auth/src/auth/session.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { expect, test } from "vitest"; -import { validateDatabaseSession } from "./session.js"; - -test("validateDatabaseSession() returns null if dead state", async () => { - const output = validateDatabaseSession({ - id: "", - idle_expires: new Date().getTime() - 10 * 1000, - active_expires: new Date().getTime(), - user_id: "" - }); - expect(output).toBeNull(); -}); - -test("validateDatabaseSession() returns idle session if idle state", async () => { - const output = validateDatabaseSession({ - id: "", - idle_expires: new Date().getTime() + 10 * 1000, - active_expires: new Date().getTime() - 10 * 1000, - user_id: "" - }); - expect(output?.state).toBe("idle"); -}); - -test("validateDatabaseSession() returns active session if active state", async () => { - const output = validateDatabaseSession({ - id: "", - idle_expires: new Date().getTime() + 10 * 1000, - active_expires: new Date().getTime() + 10 * 1000, - user_id: "" - }); - expect(output?.state).toBe("active"); -}); diff --git a/packages/lucia-auth/src/auth/session.ts b/packages/lucia-auth/src/auth/session.ts deleted file mode 100644 index 98bd4c450..000000000 --- a/packages/lucia-auth/src/auth/session.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { isWithinExpiration } from "../utils/date.js"; -import { Session } from "./index.js"; -import { SessionSchema } from "./schema.js"; - -export const validateDatabaseSession = ( - databaseSession: SessionSchema -): Session | null => { - if (!isWithinExpiration(databaseSession.idle_expires)) { - return null; - } - const activeKey = isWithinExpiration(databaseSession.active_expires); - return { - sessionId: databaseSession.id, - userId: databaseSession.user_id, - activePeriodExpiresAt: new Date(Number(databaseSession.active_expires)), - idlePeriodExpiresAt: new Date(Number(databaseSession.idle_expires)), - state: activeKey ? "active" : "idle", - fresh: false - }; -}; diff --git a/packages/lucia-auth/src/index.ts b/packages/lucia-auth/src/index.ts deleted file mode 100644 index f384dba3e..000000000 --- a/packages/lucia-auth/src/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -export { lucia as default } from "./auth/index.js"; -export { SESSION_COOKIE_NAME, Cookie } from "./auth/cookie.js"; -export { LuciaError, LuciaErrorConstructor } from "./auth/error.js"; -export { generateRandomString } from "./utils/nanoid.js"; -export { serializeCookie } from "./utils/cookie.js"; - -export type GlobalAuth = Lucia.Auth; -export type GlobalUserAttributes = Lucia.UserAttributes; - -export type { - User, - Key, - Session, - SingleUseKey, - Configuration, - PersistentKey, - Env, - Auth -} from "./auth/index.js"; -export type { - Adapter, - AdapterFunction, - UserAdapter, - SessionAdapter -} from "./auth/adapter.js"; -export type { UserSchema, KeySchema, SessionSchema } from "./auth/schema.js"; -export type { - RequestContext, - Middleware, - AuthRequest -} from "./auth/request.js"; diff --git a/packages/integration-oauth/.gitignore b/packages/lucia/.gitignore similarity index 87% rename from packages/integration-oauth/.gitignore rename to packages/lucia/.gitignore index 69ab5abde..7a0ae98eb 100644 --- a/packages/integration-oauth/.gitignore +++ b/packages/lucia/.gitignore @@ -2,3 +2,4 @@ /dist .DS_Store .env +*.tgz \ No newline at end of file diff --git a/packages/integration-oauth/.prettierignore b/packages/lucia/.prettierignore similarity index 100% rename from packages/integration-oauth/.prettierignore rename to packages/lucia/.prettierignore diff --git a/packages/lucia-auth/CHANGELOG.md b/packages/lucia/CHANGELOG.md similarity index 100% rename from packages/lucia-auth/CHANGELOG.md rename to packages/lucia/CHANGELOG.md diff --git a/packages/lucia-auth/README.md b/packages/lucia/README.md similarity index 100% rename from packages/lucia-auth/README.md rename to packages/lucia/README.md diff --git a/packages/lucia/package.json b/packages/lucia/package.json new file mode 100644 index 000000000..f542be58c --- /dev/null +++ b/packages/lucia/package.json @@ -0,0 +1,59 @@ +{ + "name": "lucia", + "version": "1.0.0", + "description": "A simple and flexible authentication library", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "module": "dist/index.js", + "svelte": "dist/index.js", + "type": "module", + "files": [ + "/dist/", + "CHANGELOG.md" + ], + "scripts": { + "build": "shx rm -rf ./dist/* && tsc", + "auri.publish": "pnpm build && pnpm publish --no-git-checks --access public", + "test": "vitest run", + "test.debug": "vitest run src/utils/debug.test.ts" + }, + "keywords": [ + "lucia", + "lucia", + "authentication", + "auth" + ], + "exports": { + ".": "./dist/index.js", + "./middleware": "./dist/middleware/index.js", + "./polyfill/node": "./dist/polyfill/node.js", + "./utils": "./dist/utils/index.js" + }, + "typesVersions": { + "*": { + "middleware": [ + "dist/middleware/index.d.ts" + ], + "polyfill/node": [ + "dist/polyfill/node.d.ts" + ], + "polyfill/utils": [ + "dist/utils/index.d.ts" + ] + } + }, + "repository": { + "type": "git", + "url": "https://github.com/pilcrowOnPaper/lucia", + "directory": "packages/lucia" + }, + "author": "pilcrowonpaper", + "license": "MIT", + "devDependencies": { + "@sveltejs/kit": "1.10.0", + "@types/express": "^4.17.17", + "@types/node": "^18.6.2", + "prettier": "^2.3.0", + "vitest": "^0.30.1" + } +} diff --git a/packages/lucia-auth/src/auth/adapter.ts b/packages/lucia/src/auth/adapter.ts similarity index 54% rename from packages/lucia-auth/src/auth/adapter.ts rename to packages/lucia/src/auth/adapter.ts index 7f4924ae7..1afa49994 100644 --- a/packages/lucia-auth/src/auth/adapter.ts +++ b/packages/lucia/src/auth/adapter.ts @@ -1,49 +1,44 @@ import type { LuciaErrorConstructor } from "../index.js"; import type { UserSchema, SessionSchema, KeySchema } from "./schema.js"; -export type AdapterFunction = - (E: LuciaErrorConstructor) => T; +export type InitializeAdapter< + T extends Adapter | UserAdapter | SessionAdapter +> = (E: LuciaErrorConstructor) => T; export type Adapter = Readonly< { - getSessionAndUserBySessionId?: (sessionId: string) => Promise<{ - user: UserSchema; - session: SessionSchema; - } | null>; + getSessionAndUser?: ( + sessionId: string + ) => Promise<[SessionSchema, UserSchema] | [null, null]>; } & SessionAdapter & UserAdapter >; export type UserAdapter = Readonly<{ getUser: (userId: string) => Promise; - setUser: ( + setUser: (user: UserSchema, key: KeySchema | null) => Promise; + updateUser: ( userId: string, - userAttributes: Record, - key: KeySchema | null - ) => Promise; + partialUser: Partial + ) => Promise; deleteUser: (userId: string) => Promise; - updateUserAttributes: ( - userId: string, - attributes: Record - ) => Promise; + + getKey: (keyId: string) => Promise; + getKeysByUserId: (userId: string) => Promise; setKey: (key: KeySchema) => Promise; - deleteNonPrimaryKey: (keyId: string) => Promise; + updateKey: (keyId: string, partialKey: Partial) => Promise; + deleteKey: (keyId: string) => Promise; deleteKeysByUserId: (userId: string) => Promise; - updateKeyPassword: ( - keyId: string, - hashedPassword: string | null - ) => Promise; - getKey: ( - keyId: string, - shouldDataBeDeleted: (key: KeySchema) => Promise - ) => Promise; - getKeysByUserId: (userId: string) => Promise; }>; export type SessionAdapter = Readonly<{ getSession: (sessionId: string) => Promise; getSessionsByUserId: (userId: string) => Promise; setSession: (session: SessionSchema) => Promise; + updateSession: ( + sessionId: string, + partialSession: Partial + ) => Promise; deleteSession: (sessionId: string) => Promise; deleteSessionsByUserId: (userId: string) => Promise; }>; diff --git a/packages/lucia/src/auth/cookie.ts b/packages/lucia/src/auth/cookie.ts new file mode 100644 index 000000000..8488b6e6a --- /dev/null +++ b/packages/lucia/src/auth/cookie.ts @@ -0,0 +1,46 @@ +import { serializeCookie } from "../utils/cookie.js"; + +import type { Env, Session } from "./index.js"; +import type { CookieAttributes } from "../utils/cookie.js"; + +export const DEFAULT_SESSION_COOKIE_NAME = "auth_session"; + +export type SessionCookieAttributes = { + sameSite?: "strict" | "lax"; + path?: string; + domain?: string; +}; + +export const createSessionCookie = ( + session: Session | null, + env: Env, + cookieConfig: { + name: string; + attributes: SessionCookieAttributes; + } +) => { + return new Cookie( + cookieConfig.name ?? DEFAULT_SESSION_COOKIE_NAME, + session?.sessionId ?? "", + { + ...cookieConfig.attributes, + httpOnly: true, + expires: new Date(session?.idlePeriodExpiresAt ?? 0), + secure: env === "PROD" + } + ); +}; + +export class Cookie { + constructor(name: string, value: string, options: CookieAttributes) { + this.name = name; + this.value = value; + this.attributes = options; + } + public readonly name: string; + public readonly value: string; + public readonly attributes: CookieAttributes; + public readonly serialize = () => { + return serializeCookie(this.name, this.value, this.attributes); + }; +} diff --git a/packages/lucia-auth/src/auth/error.ts b/packages/lucia/src/auth/error.ts similarity index 88% rename from packages/lucia-auth/src/auth/error.ts rename to packages/lucia/src/auth/error.ts index 5c184c81e..dce70419a 100644 --- a/packages/lucia-auth/src/auth/error.ts +++ b/packages/lucia/src/auth/error.ts @@ -17,7 +17,6 @@ export type LuciaErrorConstructor = Constructor; export type ErrorMessage = | "AUTH_INVALID_SESSION_ID" | "AUTH_INVALID_PASSWORD" - | "AUTH_DUPLICATE_SESSION_ID" | "AUTH_DUPLICATE_KEY_ID" | "AUTH_INVALID_KEY_ID" | "AUTH_INVALID_USER_ID" @@ -26,5 +25,4 @@ export type ErrorMessage = | "REQUEST_UNAUTHORIZED" | "UNKNOWN_ERROR" | "AUTH_OUTDATED_PASSWORD" - | "AUTO_USER_ID_GENERATION_NOT_SUPPORTED" - | "AUTH_EXPIRED_KEY"; + diff --git a/packages/lucia/src/auth/index.ts b/packages/lucia/src/auth/index.ts new file mode 100644 index 000000000..a7d05fb99 --- /dev/null +++ b/packages/lucia/src/auth/index.ts @@ -0,0 +1,696 @@ +import { DEFAULT_SESSION_COOKIE_NAME, createSessionCookie } from "./cookie.js"; +import { logError } from "../utils/log.js"; +import { generateScryptHash, validateScryptHash } from "../utils/crypto.js"; +import { generateRandomString } from "../utils/nanoid.js"; +import { LuciaError } from "./error.js"; +import { parseCookie } from "../utils/cookie.js"; +import { isValidDatabaseSession } from "./session.js"; +import { transformDatabaseKey } from "./key.js"; +import { AuthRequest } from "./request.js"; +import { lucia as defaultMiddleware } from "../middleware/index.js"; +import { debug } from "../utils/debug.js"; +import { isWithinExpiration } from "../utils/date.js"; + +import type { Cookie, SessionCookieAttributes } from "./cookie.js"; +import type { UserSchema, SessionSchema } from "./schema.js"; +import type { Adapter, SessionAdapter, InitializeAdapter } from "./adapter.js"; +import type { Middleware, LuciaRequest } from "./request.js"; + +export type Session = Readonly<{ + user: User; + sessionId: string; + activePeriodExpiresAt: Date; + idlePeriodExpiresAt: Date; + state: "idle" | "active"; + fresh: boolean; +}> & + ReturnType; + +export type Key = Readonly<{ + userId: string; + providerId: string; + providerUserId: string; + passwordDefined: boolean; +}>; + +export type Env = "DEV" | "PROD"; + +export type User = { + userId: string; +} & ReturnType; + +export const lucia = <_Configuration extends Configuration>( + config: _Configuration +) => { + return new Auth(config); +}; + +const validateConfiguration = (config: Configuration) => { + const adapterProvided = config.adapter; + if (!adapterProvided) { + logError('Adapter is not defined in configuration ("config.adapter")'); + process.exit(1); + } +}; + +const defaultSessionCookieAttributes: SessionCookieAttributes = { + sameSite: "lax", + path: "/" +}; + +export class Auth<_Configuration extends Configuration = any> { + private adapter: Adapter; + private generateUserId: () => MaybePromise; + private sessionCookieName: string; + private sessionCookieAttributes: SessionCookieAttributes; + private sessionExpiresIn: { + activePeriod: number; + idlePeriod: number; + }; + private env: Env; + private passwordHash: { + generate: (s: string) => MaybePromise; + validate: (s: string, hash: string) => MaybePromise; + }; + protected middleware: _Configuration["middleware"] extends Middleware + ? _Configuration["middleware"] + : ReturnType; + private csrfProtectionEnabled: boolean; + private requestOrigins: string[]; + private experimental: { + debugMode: boolean; + }; + + constructor(config: _Configuration) { + validateConfiguration(config); + + if ("user" in config.adapter) { + let userAdapter = config.adapter.user(LuciaError); + let sessionAdapter = config.adapter.session(LuciaError); + if ("getSessionAndUserBySessionId" in userAdapter) { + const { getSessionAndUserBySessionId: _, ...extractedUserAdapter } = + userAdapter; + userAdapter = extractedUserAdapter; + } + if ("getSessionAndUserBySessionId" in sessionAdapter) { + const { getSessionAndUserBySessionId: _, ...extractedSessionAdapter } = + sessionAdapter; + sessionAdapter = extractedSessionAdapter; + } + this.adapter = { + ...userAdapter, + ...sessionAdapter + }; + } else { + this.adapter = config.adapter(LuciaError); + } + this.generateUserId = + config.generateUserId ?? (() => generateRandomString(15)); + this.env = config.env; + this.csrfProtectionEnabled = config.csrfProtection ?? true; + this.sessionExpiresIn = { + activePeriod: + config.sessionExpiresIn?.activePeriod ?? 1000 * 60 * 60 * 24, + idlePeriod: + config.sessionExpiresIn?.idlePeriod ?? 1000 * 60 * 60 * 24 * 14 + }; + this.getUserAttributes = (databaseUser) => { + const defaultTransform = () => { + return {} as any; + }; + const transform = config.getUserAttributes ?? defaultTransform; + return transform(databaseUser); + }; + this.getSessionAttributes = (databaseSession) => { + const defaultTransform = () => { + return {} as any; + }; + const transform = config.getSessionAttributes ?? defaultTransform; + return transform(databaseSession); + }; + this.sessionCookieName = + config.sessionCookie?.name ?? DEFAULT_SESSION_COOKIE_NAME; + this.sessionCookieAttributes = + config.sessionCookie?.attributes ?? defaultSessionCookieAttributes; + this.passwordHash = { + generate: config.passwordHash?.generate ?? generateScryptHash, + validate: config.passwordHash?.validate ?? validateScryptHash + }; + this.middleware = config.middleware ?? defaultMiddleware(); + this.requestOrigins = config.requestOrigins ?? []; + this.experimental = { + debugMode: config.experimental?.debugMode ?? false + }; + + debug.init(this.experimental.debugMode); + } + + protected getUserAttributes: ( + databaseUser: UserSchema + ) => _Configuration extends Configuration + ? _UserAttributes + : never; + + protected getSessionAttributes: ( + databaseSession: SessionSchema + ) => _Configuration extends Configuration + ? _SessionAttributes + : never; + + public transformDatabaseUser = (databaseUser: UserSchema): User => { + const attributes = this.getUserAttributes(databaseUser); + return { + ...attributes, + userId: databaseUser.id + }; + }; + + public transformDatabaseSession = ( + databaseSession: SessionSchema, + user: User + ): Session => { + const attributes = this.getSessionAttributes(databaseSession); + const active = isWithinExpiration(databaseSession.active_expires); + return { + ...attributes, + user, + sessionId: databaseSession.id, + activePeriodExpiresAt: new Date(Number(databaseSession.active_expires)), + idlePeriodExpiresAt: new Date(Number(databaseSession.idle_expires)), + state: active ? "active" : "idle", + fresh: false + }; + }; + + private getDatabaseUser = async (userId: string): Promise => { + const databaseUser = await this.adapter.getUser(userId); + if (!databaseUser) { + throw new LuciaError("AUTH_INVALID_USER_ID"); + } + return databaseUser; + }; + + private getDatabaseSession = async ( + sessionId: string + ): Promise => { + const databaseSession = await this.adapter.getSession(sessionId); + if (!databaseSession) { + debug.session.fail("Session not found", sessionId); + throw new LuciaError("AUTH_INVALID_SESSION_ID"); + } + if (!isValidDatabaseSession(databaseSession)) { + debug.session.fail( + `Session expired at ${new Date(Number(databaseSession.idle_expires))}`, + sessionId + ); + throw new LuciaError("AUTH_INVALID_SESSION_ID"); + } + return databaseSession; + }; + + private getDatabaseSessionAndUser = async ( + sessionId: string + ): Promise<[SessionSchema, UserSchema]> => { + if (this.adapter.getSessionAndUser) { + const [databaseSession, databaseUser] = + await this.adapter.getSessionAndUser(sessionId); + if (!databaseSession) { + debug.session.fail("Session not found", sessionId); + throw new LuciaError("AUTH_INVALID_SESSION_ID"); + } + if (!isValidDatabaseSession(databaseSession)) { + debug.session.fail( + `Session expired at ${new Date( + Number(databaseSession.idle_expires) + )}`, + sessionId + ); + throw new LuciaError("AUTH_INVALID_SESSION_ID"); + } + return [databaseSession, databaseUser]; + } + const databaseSession = await this.getDatabaseSession(sessionId); + const databaseUser = await this.getDatabaseUser(databaseSession.user_id); + return [databaseSession, databaseUser]; + }; + + private validateSessionIdArgument = (sessionId: string) => { + if (sessionId.length !== 40) { + debug.session.fail("Expected id length to be 40", sessionId); + throw new LuciaError("AUTH_INVALID_SESSION_ID"); + } + }; + + private generateSessionId = (sessionExpiresIn?: { + activePeriod: number; + idlePeriod: number; + }): readonly [ + sessionId: string, + activePeriodExpiresAt: Date, + idlePeriodExpiresAt: Date + ] => { + const sessionId = generateRandomString(40); + const activePeriodExpiresAt = new Date( + new Date().getTime() + + (sessionExpiresIn?.activePeriod ?? this.sessionExpiresIn.activePeriod) + ); + const idlePeriodExpiresAt = new Date( + activePeriodExpiresAt.getTime() + + (sessionExpiresIn?.idlePeriod ?? this.sessionExpiresIn.idlePeriod) + ); + return [sessionId, activePeriodExpiresAt, idlePeriodExpiresAt]; + }; + + public getUser = async (userId: string): Promise => { + const databaseUser = await this.getDatabaseUser(userId); + const user = this.transformDatabaseUser(databaseUser); + return user; + }; + + public createUser = async (data: { + key: { + providerId: string; + providerUserId: string; + password: string | null; + } | null; + attributes: Lucia.DatabaseUserAttributes; + }): Promise => { + const userId = await this.generateUserId(); + const userAttributes = data.attributes ?? {}; + const databaseUser = { + ...userAttributes, + id: userId + } satisfies UserSchema; + if (data.key === null) { + await this.adapter.setUser(databaseUser, null); + return this.transformDatabaseUser(databaseUser); + } + const keyId = `${data.key.providerId}:${data.key.providerUserId}`; + const password = data.key.password; + const hashedPassword = password + ? await this.passwordHash.generate(password) + : null; + await this.adapter.setUser(databaseUser, { + id: keyId, + user_id: userId, + hashed_password: hashedPassword + }); + return this.transformDatabaseUser(databaseUser); + }; + + public updateUserAttributes = async ( + userId: string, + attributes: Partial + ): Promise => { + await this.adapter.updateUser(userId, attributes); + return await this.getUser(userId); + }; + + public deleteUser = async (userId: string): Promise => { + await this.adapter.deleteSessionsByUserId(userId); + await this.adapter.deleteKeysByUserId(userId); + await this.adapter.deleteUser(userId); + }; + + public useKey = async ( + providerId: string, + providerUserId: string, + password: string | null + ): Promise => { + const keyId = `${providerId}:${providerUserId}`; + const databaseKey = await this.adapter.getKey(keyId); + if (!databaseKey) { + debug.key.fail("Key not found", keyId); + throw new LuciaError("AUTH_INVALID_KEY_ID"); + } + const hashedPassword = databaseKey.hashed_password; + if (hashedPassword) { + debug.key.info("Key includes password"); + if (!password) { + debug.key.fail("Key password not provided", keyId); + throw new LuciaError("AUTH_INVALID_PASSWORD"); + } + if (hashedPassword.startsWith("$2a")) { + throw new LuciaError("AUTH_OUTDATED_PASSWORD"); + } + const validPassword = await this.passwordHash.validate( + password, + hashedPassword + ); + if (!validPassword) { + debug.key.fail("Incorrect key password", password); + throw new LuciaError("AUTH_INVALID_PASSWORD"); + } + debug.key.notice("Validated key password"); + } else { + debug.key.info("No password included in key"); + } + debug.key.success("Validated key", keyId); + return transformDatabaseKey(databaseKey); + }; + + public getSession = async (sessionId: string): Promise => { + this.validateSessionIdArgument(sessionId); + const [databaseSession, databaseUser] = + await this.getDatabaseSessionAndUser(sessionId); + const user = this.transformDatabaseUser(databaseUser); + return this.transformDatabaseSession(databaseSession, user); + }; + + public getAllUserSessions = async (userId: string): Promise => { + const [user, databaseSessions] = await Promise.all([ + this.getUser(userId), + await this.adapter.getSessionsByUserId(userId) + ]); + const validStoredUserSessions = databaseSessions + .filter((databaseSession) => { + return isValidDatabaseSession(databaseSession); + }) + .map((databaseSession) => { + return this.transformDatabaseSession(databaseSession, user); + }); + return validStoredUserSessions; + }; + + public validateSession = async (sessionId: string): Promise => { + this.validateSessionIdArgument(sessionId); + const [databaseSession, databaseUser] = + await this.getDatabaseSessionAndUser(sessionId); + const user = this.transformDatabaseUser(databaseUser); + const session = this.transformDatabaseSession(databaseSession, user); + if (session.state === "active") { + debug.session.success("Validated session", session.sessionId); + return session; + } + const [newSessionId, activePeriodExpiresAt, idlePeriodExpiresAt] = + this.generateSessionId(); + const renewedDatabaseSession = { + ...databaseSession, + id: newSessionId, + active_expires: activePeriodExpiresAt.getTime(), + idle_expires: idlePeriodExpiresAt.getTime() + } satisfies SessionSchema; + await Promise.all([ + this.adapter.setSession(renewedDatabaseSession), + this.adapter.deleteSession(sessionId) + ]); + return this.transformDatabaseSession(renewedDatabaseSession, user); + }; + + public createSession = async < + // for some absurd reasons + // this needs to be a generic before doing a conditional check + // to work when exported + _Attributes extends Lucia.DatabaseSessionAttributes = Lucia.DatabaseSessionAttributes + >( + ...args: _Attributes extends EmptyObject + ? [ + userId: string, + // options args is optional if no database session attributes are defined + options?: { + attributes?: Lucia.DatabaseSessionAttributes; + } + ] + : [ + userId: string, + options: { + attributes: Lucia.DatabaseSessionAttributes; + } + ] + ): Promise => { + const [sessionId, activePeriodExpiresAt, idlePeriodExpiresAt] = + this.generateSessionId(); + const [userId, options] = args; + const attributes = options?.attributes ?? {}; + const databaseSession = { + ...attributes, + id: sessionId, + user_id: userId, + active_expires: activePeriodExpiresAt.getTime(), + idle_expires: idlePeriodExpiresAt.getTime() + } satisfies SessionSchema; + const [user] = await Promise.all([ + this.getUser(userId), + this.adapter.setSession(databaseSession) + ]); + return this.transformDatabaseSession(databaseSession, user); + }; + + public renewSession = async (sessionId: string): Promise => { + this.validateSessionIdArgument(sessionId); + const [databaseSession, databaseUser] = + await this.getDatabaseSessionAndUser(sessionId); + const user = this.transformDatabaseUser(databaseUser); + const [newSessionId, activePeriodExpiresAt, idlePeriodExpiresAt] = + this.generateSessionId(); + const renewedDatabaseSession = { + ...databaseSession, + id: newSessionId, + active_expires: activePeriodExpiresAt.getTime(), + idle_expires: idlePeriodExpiresAt.getTime() + } satisfies SessionSchema; + await Promise.all([ + this.adapter.setSession(renewedDatabaseSession), + this.adapter.deleteSession(sessionId) + ]); + return this.transformDatabaseSession(renewedDatabaseSession, user); + }; + + public updateSessionAttributes = async ( + sessionId: string, + attributes: Partial + ): Promise => { + this.validateSessionIdArgument(sessionId); + await this.adapter.updateSession(sessionId, attributes); + return this.getSession(sessionId); + }; + + public invalidateSession = async (sessionId: string): Promise => { + this.validateSessionIdArgument(sessionId); + await this.adapter.deleteSession(sessionId); + debug.session.notice("Invalidated session", sessionId); + }; + + public invalidateAllUserSessions = async (userId: string): Promise => { + await this.adapter.deleteSessionsByUserId(userId); + }; + + public deleteDeadUserSessions = async (userId: string): Promise => { + const databaseSessions = await this.adapter.getSessionsByUserId(userId); + const deadSessionIds = databaseSessions + .filter((databaseSession) => { + return !isValidDatabaseSession(databaseSession); + }) + .map((databaseSession) => databaseSession.id); + await Promise.all( + deadSessionIds.map((deadSessionId) => { + this.adapter.deleteSession(deadSessionId); + }) + ); + }; + + public validateRequestOrigin = ( + request: Omit & { + headers: Pick; + } + ): void => { + if (request.method === null) { + debug.request.fail("Request method unavailable"); + throw new LuciaError("AUTH_INVALID_REQUEST"); + } + if (request.url === null) { + debug.request.fail("Request url unavailable"); + throw new LuciaError("AUTH_INVALID_REQUEST"); + } + const csrfCheckRequired = + request.method.toUpperCase() !== "GET" && + request.method.toUpperCase() !== "HEAD"; + if (this.csrfProtectionEnabled && csrfCheckRequired) { + const requestOrigin = request.headers.origin; + if (!requestOrigin) { + debug.request.fail("No request origin available"); + throw new LuciaError("AUTH_INVALID_REQUEST"); + } + try { + const url = new URL(request.url); + if (![url.origin, ...this.requestOrigins].includes(requestOrigin)) { + debug.request.fail("Invalid request origin", requestOrigin); + throw new LuciaError("AUTH_INVALID_REQUEST"); + } + debug.request.info("Valid request origin", requestOrigin); + } catch { + debug.request.fail("Invalid origin string", requestOrigin); + // failed to parse url + throw new LuciaError("AUTH_INVALID_REQUEST"); + } + } else { + debug.request.notice("Skipping CSRF check"); + } + }; + + public readSessionCookie = (request: LuciaRequest): string | null => { + if (typeof request.storedSessionCookie === "string") { + return request.storedSessionCookie; + } + const cookies = parseCookie(request.headers.cookie ?? ""); + const sessionId = cookies[this.sessionCookieName] ?? null; + if (sessionId) { + debug.request.info("Found session cookie", sessionId); + } else { + debug.request.info("No session cookie found"); + } + return sessionId; + }; + + public readBearerToken = (request: LuciaRequest): string | null => { + const authorizationHeader = request.headers.authorization; + if (!authorizationHeader) { + debug.request.info("No token found in authorization header"); + return null; + } + const [authScheme, token] = authorizationHeader.split(" ") as [ + string, + string | undefined + ]; + if (authScheme !== "Bearer") { + debug.request.fail( + "Invalid authorization header auth scheme", + authScheme + ); + return null; + } + return token ?? null; + }; + + public handleRequest = ( + // cant reference middleware type with Lucia.Auth + ...args: Auth<_Configuration>["middleware"] extends Middleware + ? Args + : never + ): AuthRequest => { + const middleware = this.middleware as Middleware; + return new AuthRequest( + this, + middleware({ + args, + env: this.env, + cookieName: this.sessionCookieName + }) + ); + }; + + public createSessionCookie = (session: Session | null): Cookie => { + return createSessionCookie(session, this.env, { + name: this.sessionCookieName, + attributes: this.sessionCookieAttributes + }); + }; + + public createKey = async ( + userId: string, + keyData: { + providerId: string; + providerUserId: string; + password: string | null; + } + ): Promise => { + const keyId = `${keyData.providerId}:${keyData.providerUserId}`; + let hashedPassword: string | null = null; + if (keyData.password !== null) { + hashedPassword = await this.passwordHash.generate(keyData.password); + } + await this.adapter.setKey({ + id: keyId, + user_id: userId, + hashed_password: hashedPassword + }); + return { + providerId: keyData.providerId, + providerUserId: keyData.providerUserId, + passwordDefined: !!keyData.password, + userId + } satisfies Key as any; + }; + + public deleteKey = async ( + providerId: string, + providerUserId: string + ): Promise => { + const keyId = `${providerId}:${providerUserId}`; + await this.adapter.deleteKey(keyId); + }; + + public getKey = async ( + providerId: string, + providerUserId: string + ): Promise => { + const keyId = `${providerId}:${providerUserId}`; + const databaseKey = await this.adapter.getKey(keyId); + if (!databaseKey) { + throw new LuciaError("AUTH_INVALID_KEY_ID"); + } + const key = transformDatabaseKey(databaseKey); + return key; + }; + + public getAllUserKeys = async (userId: string): Promise => { + const [databaseKeys] = await Promise.all([ + await this.adapter.getKeysByUserId(userId), + this.getUser(userId) + ]); + return databaseKeys.map((databaseKey) => transformDatabaseKey(databaseKey)); + }; + + public updateKeyPassword = async ( + providerId: string, + providerUserId: string, + password: string | null + ): Promise => { + const keyId = `${providerId}:${providerUserId}`; + const hashedPassword = + password === null ? null : await this.passwordHash.generate(password); + await this.adapter.updateKey(keyId, { + hashed_password: hashedPassword + }); + await this.getKey(providerId, providerUserId); + }; +} + +type MaybePromise = T | Promise; + +export type Configuration< + _UserAttributes extends Record = {}, + _SessionAttributes extends Record = {} +> = { + adapter: + | InitializeAdapter + | { + user: InitializeAdapter; + session: InitializeAdapter; + }; + env: Env; + + middleware?: Middleware; + csrfProtection?: boolean; + requestOrigins?: string[]; + sessionExpiresIn: { + activePeriod: number; + idlePeriod: number; + }; + sessionCookie?: { + name?: string; + attributes?: SessionCookieAttributes; + }; + getSessionAttributes?: (databaseSession: SessionSchema) => _SessionAttributes; + generateUserId?: () => MaybePromise; + getUserAttributes?: (databaseUser: UserSchema) => _UserAttributes; + passwordHash?: { + generate: (password: string) => MaybePromise; + validate: (password: string, hash: string) => MaybePromise; + }; + experimental?: { + debugMode?: boolean; + }; +}; + +type EmptyObject = Record; diff --git a/packages/lucia/src/auth/key.ts b/packages/lucia/src/auth/key.ts new file mode 100644 index 000000000..b4fafa999 --- /dev/null +++ b/packages/lucia/src/auth/key.ts @@ -0,0 +1,15 @@ +import type { KeySchema } from "./schema.js"; +import type { Key } from "./index.js"; + +export const transformDatabaseKey = (databaseKey: KeySchema): Key => { + const [providerId, ...providerUserIdSegments] = databaseKey.id.split(":"); + const providerUserId = providerUserIdSegments.join(":"); + const userId = databaseKey.user_id; + const isPasswordDefined = !!databaseKey.hashed_password; + return { + providerId, + providerUserId, + userId, + passwordDefined: isPasswordDefined + }; +}; diff --git a/packages/lucia-auth/src/auth/request.ts b/packages/lucia/src/auth/request.ts similarity index 55% rename from packages/lucia-auth/src/auth/request.ts rename to packages/lucia/src/auth/request.ts index d83ca7e60..b116e8c69 100644 --- a/packages/lucia-auth/src/auth/request.ts +++ b/packages/lucia/src/auth/request.ts @@ -1,23 +1,28 @@ -import { Cookie } from "./cookie.js"; -import type { Auth, Session, User } from "./index.js"; import { debug } from "../utils/debug.js"; +import type { Auth, Env, Session } from "./index.js"; +import type { Cookie } from "./cookie.js"; + export type LuciaRequest = { method: string; url: string; headers: { origin: string | null; cookie: string | null; + authorization: string | null; }; + storedSessionCookie?: string | null }; export type RequestContext = { request: LuciaRequest; setCookie: (cookie: Cookie) => void; }; -export type Middleware = ( - ...args: [...Args, "DEV" | "PROD"] -) => RequestContext; +export type Middleware = (context: { + args: Args; + env: Env; + cookieName: string; +}) => RequestContext; export class AuthRequest { private auth: A; @@ -26,26 +31,22 @@ export class AuthRequest { this.auth = auth; this.context = context; try { - this.storedSessionId = auth.parseRequestHeaders(context.request); + auth.validateRequestOrigin(context.request); + this.storedSessionId = auth.readSessionCookie(context.request); } catch (e) { this.storedSessionId = null; } + this.bearerToken = auth.readBearerToken(context.request); } private validatePromise: Promise | null = null; - private validateUserPromise: Promise< - | { user: User; session: Session } - | { - user: null; - session: null; - } - > | null = null; - public storedSessionId: string | null; + private validateBearerTokenPromise: Promise | null = null; + private storedSessionId: string | null; + private bearerToken: string | null; public setSession = (session: Session | null) => { const sessionId = session?.sessionId ?? null; if (this.storedSessionId === sessionId) return; - this.validateUserPromise = null; this.validatePromise = null; this.setSessionCookie(session); }; @@ -72,63 +73,38 @@ export class AuthRequest { return this.validatePromise; } this.validatePromise = new Promise(async (resolve) => { - if (!this.storedSessionId) { - return resolve(null); - } + if (!this.storedSessionId) return resolve(null); try { const session = await this.auth.validateSession(this.storedSessionId); if (session.fresh) { this.setSessionCookie(session); } return resolve(session); - } catch (e) { + } catch { this.setSessionCookie(null); return resolve(null); } }); - return this.validatePromise; + return await this.validatePromise; }; - public validateUser = async (): Promise< - | { user: null; session: null } - | { - user: User; - session: Session; - } - > => { - const resolveNullSession = ( - resolve: (result: { user: null; session: null }) => void - ) => { - this.setSessionCookie(null); - return resolve({ - user: null, - session: null - }); - }; - - if (this.validateUserPromise) { - debug.request.info("Using cached result for session validation"); - return this.validateUserPromise; + public validateBearerToken = async (): Promise => { + if (this.validateBearerTokenPromise) { + debug.request.info("Using cached result for bearer token validation"); + return this.validatePromise; } - - this.validateUserPromise = new Promise(async (resolve) => { - if (!this.storedSessionId) { - return resolveNullSession(resolve); - } + this.validatePromise = new Promise(async (resolve) => { + if (!this.bearerToken) return resolve(null); try { - const { session, user } = await this.auth.validateSessionUser( - this.storedSessionId - ); - if (session.fresh) { - this.setSessionCookie(session); - } - return resolve({ session, user }); - } catch (e) { - return resolveNullSession(resolve); + const session = await this.auth.getSession(this.bearerToken); + if (session.state === "idle") return resolve(null); + return resolve(session); + } catch { + return resolve(null); } }); - return this.validateUserPromise; + return await this.validatePromise; }; } diff --git a/packages/lucia/src/auth/schema.ts b/packages/lucia/src/auth/schema.ts new file mode 100644 index 000000000..746335272 --- /dev/null +++ b/packages/lucia/src/auth/schema.ts @@ -0,0 +1,16 @@ +export type KeySchema = { + id: string; + hashed_password: string | null; + user_id: string; +}; + +export type UserSchema = { + id: string; +} & Lucia.DatabaseUserAttributes; + +export type SessionSchema = { + id: string; + active_expires: number; + idle_expires: number; + user_id: string; +} & Lucia.DatabaseSessionAttributes; diff --git a/packages/lucia/src/auth/session.test.ts b/packages/lucia/src/auth/session.test.ts new file mode 100644 index 000000000..0fa9567f1 --- /dev/null +++ b/packages/lucia/src/auth/session.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from "vitest"; +import { isValidDatabaseSession } from "./session.js"; + +test("isValidDatabaseSession() returns false if dead state", async () => { + const output = isValidDatabaseSession({ + id: "", + idle_expires: new Date().getTime() - 10 * 1000, + active_expires: new Date().getTime(), + user_id: "" + }); + expect(output).toBe(false); +}); diff --git a/packages/lucia/src/auth/session.ts b/packages/lucia/src/auth/session.ts new file mode 100644 index 000000000..0abe5c5ea --- /dev/null +++ b/packages/lucia/src/auth/session.ts @@ -0,0 +1,7 @@ +import { isWithinExpiration } from "../utils/date.js"; + +import type { SessionSchema } from "./schema.js"; + +export const isValidDatabaseSession = (databaseSession: SessionSchema) => { + return isWithinExpiration(databaseSession.idle_expires); +}; diff --git a/packages/lucia/src/index.ts b/packages/lucia/src/index.ts new file mode 100644 index 000000000..140d5a309 --- /dev/null +++ b/packages/lucia/src/index.ts @@ -0,0 +1,30 @@ +export { lucia } from "./auth/index.js"; +export { DEFAULT_SESSION_COOKIE_NAME } from "./auth/cookie.js"; +export { LuciaError } from "./auth/error.js"; + +export type GlobalAuth = Lucia.Auth; +export type GlobalDatabaseUserAttributes = Lucia.DatabaseUserAttributes; +export type GlobalDatabaseSessionAttributes = Lucia.DatabaseSessionAttributes; + +export type { + User, + Key, + Session, + Configuration, + Env, + Auth +} from "./auth/index.js"; +export type { + Adapter, + InitializeAdapter, + UserAdapter, + SessionAdapter +} from "./auth/adapter.js"; +export type { UserSchema, KeySchema, SessionSchema } from "./auth/schema.js"; +export type { + RequestContext, + Middleware, + AuthRequest +} from "./auth/request.js"; +export type { Cookie } from "./auth/cookie.js"; +export type { LuciaErrorConstructor } from "./auth/error.js"; diff --git a/packages/lucia-auth/src/lucia.d.ts b/packages/lucia/src/lucia.d.ts similarity index 52% rename from packages/lucia-auth/src/lucia.d.ts rename to packages/lucia/src/lucia.d.ts index 9362be3ef..1a70ea048 100644 --- a/packages/lucia-auth/src/lucia.d.ts +++ b/packages/lucia/src/lucia.d.ts @@ -1,4 +1,5 @@ declare namespace Lucia { - export type UserAttributes = {}; + export type DatabaseUserAttributes = {}; + export type DatabaseSessionAttributes = {}; export class Auth extends (await import("./auth/index.js")).Auth {} } diff --git a/packages/lucia-auth/src/middleware/index.ts b/packages/lucia/src/middleware/index.ts similarity index 70% rename from packages/lucia-auth/src/middleware/index.ts rename to packages/lucia/src/middleware/index.ts index 222541b2b..0b75cf4d7 100644 --- a/packages/lucia-auth/src/middleware/index.ts +++ b/packages/lucia/src/middleware/index.ts @@ -1,23 +1,22 @@ +import { DEFAULT_SESSION_COOKIE_NAME } from "../index.js"; + +import type { CookieAttributes } from "../utils/cookie.js"; +import type { LuciaRequest } from "../auth/request.js"; +import type { Cookie, Middleware, RequestContext } from "../index.js"; + import type { IncomingMessage, OutgoingMessage, ServerResponse } from "node:http"; -import { - SESSION_COOKIE_NAME, - type Cookie, - type Middleware, - type RequestContext -} from "../index.js"; import type { Request as ExpressRequest, Response as ExpressResponse } from "express"; -import { CookieAttributes } from "../utils/cookie.js"; -import { LuciaRequest } from "../auth/request.js"; export const node = (): Middleware<[IncomingMessage, OutgoingMessage]> => { - return (incomingMessage, outgoingMessage, env) => { + return ({ args, env }) => { + const [incomingMessage, outgoingMessage] = args; const getUrl = () => { if (!incomingMessage.headers.host) return ""; const protocol = env === "DEV" ? "http:" : "https:"; @@ -31,7 +30,8 @@ export const node = (): Middleware<[IncomingMessage, OutgoingMessage]> => { method: incomingMessage.method ?? "", headers: { origin: incomingMessage.headers.origin ?? null, - cookie: incomingMessage.headers.cookie ?? null + cookie: incomingMessage.headers.cookie ?? null, + authorization: incomingMessage.headers.authorization ?? null } }, setCookie: (cookie) => { @@ -52,14 +52,16 @@ export const node = (): Middleware<[IncomingMessage, OutgoingMessage]> => { }; export const express = (): Middleware<[ExpressRequest, ExpressResponse]> => { - return (request, response) => { + return ({ args }) => { + const [request, response] = args; const requestContext = { request: { url: `${request.protocol}://${request.hostname}${request.path}`, method: request.method, headers: { origin: request.headers.origin ?? null, - cookie: request.headers.cookie ?? null + cookie: request.headers.cookie ?? null, + authorization: request.headers.authorization ?? null } }, setCookie: (cookie) => { @@ -74,19 +76,23 @@ type SvelteKitRequestEvent = { request: Request; cookies: { set: (name: string, value: string, options?: CookieAttributes) => void; + get: (name: string) => string | undefined; }; }; export const sveltekit = (): Middleware<[SvelteKitRequestEvent]> => { - return (event) => { + return ({ args, cookieName }) => { + const [event] = args; const requestContext = { request: { url: event.request.url, method: event.request.method, headers: { - origin: event.request.headers.get("Origin") ?? null, - cookie: event.request.headers.get("Cookie") ?? null - } + origin: event.request.headers.get("Origin"), + cookie: event.request.headers.get("Cookie"), + authorization: event.request.headers.get("Authorization") + }, + storedSessionCookie: event.cookies.get(cookieName) ?? null }, setCookie: (cookie) => { event.cookies.set(cookie.name, cookie.value, cookie.attributes); @@ -100,19 +106,25 @@ type AstroAPIContext = { request: Request; cookies: { set: (name: string, value: string, options?: CookieAttributes) => void; + get: (name: string) => { + value: string; + }; }; }; export const astro = (): Middleware<[AstroAPIContext]> => { - return (context) => { + return ({ args, cookieName }) => { + const [context] = args; const requestContext = { request: { url: context.request.url, method: context.request.method, headers: { - origin: context.request.headers.get("Origin") ?? null, - cookie: context.request.headers.get("Cookie") ?? null - } + origin: context.request.headers.get("Origin"), + cookie: context.request.headers.get("Cookie"), + authorization: context.request.headers.get("Authorization") + }, + storedSessionCookie: context.cookies.get(cookieName).value || null }, setCookie: (cookie) => { context.cookies.set(cookie.name, cookie.value, cookie.attributes); @@ -126,22 +138,28 @@ type QwikRequestEvent = { request: Request; cookie: { set: (name: string, value: string, options?: CookieAttributes) => void; + get: (key: string) => { + value: string; + } | null; }; }; export const qwik = (): Middleware<[QwikRequestEvent]> => { - return (c) => { + return ({ args, cookieName }) => { + const [event] = args; const requestContext = { request: { - url: c.request.url.toString(), - method: c.request.method, + url: event.request.url.toString(), + method: event.request.method, headers: { - origin: c.request.headers.get("Origin") ?? null, - cookie: c.request.headers.get("Cookie") ?? null - } + origin: event.request.headers.get("Origin"), + cookie: event.request.headers.get("Cookie"), + authorization: event.request.headers.get("Authorization") + }, + storedSessionCookie: event.cookie.get(cookieName)?.value ?? null }, setCookie: (cookie) => { - c.cookie.set(cookie.name, cookie.value, cookie.attributes); + event.cookie.set(cookie.name, cookie.value, cookie.attributes); } } as const satisfies RequestContext; @@ -150,11 +168,12 @@ export const qwik = (): Middleware<[QwikRequestEvent]> => { }; export const lucia = (): Middleware<[RequestContext]> => { - return (requestContext) => requestContext; + return ({ args }) => args[0]; }; export const web = (): Middleware<[Request, Headers | Response]> => { - return (request, arg2) => { + return ({ args }) => { + const [request, arg2] = args; const createSetCookie = () => { if (arg2 instanceof Response) { return (cookie: Cookie) => { @@ -170,8 +189,9 @@ export const web = (): Middleware<[Request, Headers | Response]> => { url: request.url, method: request.method, headers: { - origin: request.headers.get("Origin") ?? null, - cookie: request.headers.get("Cookie") ?? null + origin: request.headers.get("Origin"), + cookie: request.headers.get("Cookie"), + authorization: request.headers.get("Authorization") } }, setCookie: createSetCookie() @@ -222,20 +242,22 @@ type NextRequest = Request & { export const nextjs = (): Middleware< [NextJsPagesServerContext | NextJsAppServerContext | { request: NextRequest }] > => { - return (serverContext, env) => { + return ({ args, cookieName, env }) => { + const [serverContext] = args; if ("cookies" in serverContext) { const cookieStore = serverContext.cookies(); - const sessionCookie = cookieStore.get(SESSION_COOKIE_NAME) ?? null; + const sessionCookie = cookieStore.get(cookieName) ?? null; const requestContext = { request: { url: serverContext.request?.url ?? "", method: serverContext.request?.method ?? "GET", headers: { origin: serverContext.request?.headers.get("Origin") ?? null, - cookie: sessionCookie - ? `${SESSION_COOKIE_NAME}=${sessionCookie.value}` - : null - } + cookie: null, + authorization: + serverContext.request?.headers.get("Authorization") ?? null + }, + storedSessionCookie: sessionCookie?.value ?? null }, setCookie: (cookie) => { try { @@ -250,17 +272,18 @@ export const nextjs = (): Middleware< } if ("request" in serverContext) { const sessionCookie = - serverContext.request.cookies.get(SESSION_COOKIE_NAME) ?? null; + serverContext.request.cookies.get(DEFAULT_SESSION_COOKIE_NAME) ?? null; const requestContext = { request: { url: serverContext.request.url, method: serverContext.request.method, headers: { origin: serverContext.request.headers.get("Origin") ?? null, - cookie: sessionCookie - ? `${SESSION_COOKIE_NAME}=${sessionCookie.value}` - : null - } + authorization: + serverContext.request?.headers.get("Authorization") ?? null, + cookie: null + }, + storedSessionCookie: sessionCookie?.value ?? null }, setCookie: () => { // ... @@ -280,7 +303,8 @@ export const nextjs = (): Middleware< method: serverContext.req.method ?? "", headers: { origin: serverContext.req.headers.origin ?? null, - cookie: serverContext.req.headers.cookie ?? null + cookie: serverContext.req.headers.cookie ?? null, + authorization: serverContext.req.headers.authorization ?? null } } satisfies LuciaRequest; const createSetCookie = () => { @@ -328,7 +352,12 @@ type H3Event = { export const h3 = (): Middleware<[H3Event]> => { const nodeMiddleware = node(); - return (context, env) => { - return nodeMiddleware(context.node.req, context.node.res, env); + return ({ args, cookieName, env }) => { + const [context] = args; + return nodeMiddleware({ + args: [context.node.req, context.node.res], + cookieName, + env + }); }; }; diff --git a/packages/lucia-auth/src/polyfill/node.ts b/packages/lucia/src/polyfill/node.ts similarity index 100% rename from packages/lucia-auth/src/polyfill/node.ts rename to packages/lucia/src/polyfill/node.ts diff --git a/packages/lucia-auth/src/scrypt/index.test.ts b/packages/lucia/src/scrypt/index.test.ts similarity index 91% rename from packages/lucia-auth/src/scrypt/index.test.ts rename to packages/lucia/src/scrypt/index.test.ts index 0f06aaef9..fd6ccbeb2 100644 --- a/packages/lucia-auth/src/scrypt/index.test.ts +++ b/packages/lucia/src/scrypt/index.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "vitest"; import scrypt from "./index.js"; import crypto from "node:crypto"; -import { generateRandomString } from "../index.js"; +import { generateRandomString } from "../utils/nanoid.js"; test("scrypt() output matches crypto", async () => { const password = generateRandomString(16); diff --git a/packages/lucia-auth/src/scrypt/index.ts b/packages/lucia/src/scrypt/index.ts similarity index 100% rename from packages/lucia-auth/src/scrypt/index.ts rename to packages/lucia/src/scrypt/index.ts diff --git a/packages/lucia-auth/src/scrypt/pbkdf.ts b/packages/lucia/src/scrypt/pbkdf.ts similarity index 100% rename from packages/lucia-auth/src/scrypt/pbkdf.ts rename to packages/lucia/src/scrypt/pbkdf.ts diff --git a/packages/lucia-auth/src/scrypt/utils.ts b/packages/lucia/src/scrypt/utils.ts similarity index 100% rename from packages/lucia-auth/src/scrypt/utils.ts rename to packages/lucia/src/scrypt/utils.ts diff --git a/packages/lucia-auth/src/utils/cookie.ts b/packages/lucia/src/utils/cookie.ts similarity index 100% rename from packages/lucia-auth/src/utils/cookie.ts rename to packages/lucia/src/utils/cookie.ts diff --git a/packages/lucia-auth/src/utils/crypto.test.ts b/packages/lucia/src/utils/crypto.test.ts similarity index 100% rename from packages/lucia-auth/src/utils/crypto.test.ts rename to packages/lucia/src/utils/crypto.test.ts diff --git a/packages/lucia-auth/src/utils/crypto.ts b/packages/lucia/src/utils/crypto.ts similarity index 97% rename from packages/lucia-auth/src/utils/crypto.ts rename to packages/lucia/src/utils/crypto.ts index 01b1ed9ff..3bdf1dd27 100644 --- a/packages/lucia-auth/src/utils/crypto.ts +++ b/packages/lucia/src/utils/crypto.ts @@ -1,5 +1,4 @@ import scrypt from "../scrypt/index.js"; -import { debug } from "./debug.js"; import { generateRandomString } from "./nanoid.js"; export const generateScryptHash = async (s: string) => { diff --git a/packages/lucia-auth/src/utils/date.test.ts b/packages/lucia/src/utils/date.test.ts similarity index 100% rename from packages/lucia-auth/src/utils/date.test.ts rename to packages/lucia/src/utils/date.test.ts diff --git a/packages/lucia-auth/src/utils/date.ts b/packages/lucia/src/utils/date.ts similarity index 100% rename from packages/lucia-auth/src/utils/date.ts rename to packages/lucia/src/utils/date.ts diff --git a/packages/lucia-auth/src/utils/debug.test.ts b/packages/lucia/src/utils/debug.test.ts similarity index 100% rename from packages/lucia-auth/src/utils/debug.test.ts rename to packages/lucia/src/utils/debug.test.ts diff --git a/packages/lucia-auth/src/utils/debug.ts b/packages/lucia/src/utils/debug.ts similarity index 97% rename from packages/lucia-auth/src/utils/debug.ts rename to packages/lucia/src/utils/debug.ts index b99cbe8f4..07a8c20e4 100644 --- a/packages/lucia-auth/src/utils/debug.ts +++ b/packages/lucia/src/utils/debug.ts @@ -94,9 +94,7 @@ export const debug = { enableDebugMode(); linebreak(); console.log( - ` ${bg.lucia(bold(fg.white(" lucia ")))} ${fg.lucia( - bold("Debug mode enabled") - )}` + ` ${bg.lucia(bold(fg.white(" lucia ")))} ${fg.lucia(bold("Debug mode enabled"))}` ); } else { disableDebugMode(); diff --git a/packages/lucia/src/utils/index.ts b/packages/lucia/src/utils/index.ts new file mode 100644 index 000000000..9a22a5116 --- /dev/null +++ b/packages/lucia/src/utils/index.ts @@ -0,0 +1,3 @@ +export { generateRandomString } from "./nanoid.js"; +export { serializeCookie } from "./cookie.js"; +export { isWithinExpiration } from "./date.js"; diff --git a/packages/lucia-auth/src/utils/log.ts b/packages/lucia/src/utils/log.ts similarity index 100% rename from packages/lucia-auth/src/utils/log.ts rename to packages/lucia/src/utils/log.ts diff --git a/packages/lucia-auth/src/utils/nanoid.ts b/packages/lucia/src/utils/nanoid.ts similarity index 100% rename from packages/lucia-auth/src/utils/nanoid.ts rename to packages/lucia/src/utils/nanoid.ts diff --git a/packages/lucia-auth/tsconfig.json b/packages/lucia/tsconfig.json similarity index 100% rename from packages/lucia-auth/tsconfig.json rename to packages/lucia/tsconfig.json diff --git a/packages/lucia-auth/vitest.config.ts b/packages/lucia/vitest.config.ts similarity index 100% rename from packages/lucia-auth/vitest.config.ts rename to packages/lucia/vitest.config.ts diff --git a/packages/integration-oauth/.eslintignore b/packages/oauth/.eslintignore similarity index 100% rename from packages/integration-oauth/.eslintignore rename to packages/oauth/.eslintignore diff --git a/packages/integration-tokens/.gitignore b/packages/oauth/.gitignore similarity index 85% rename from packages/integration-tokens/.gitignore rename to packages/oauth/.gitignore index 69ab5abde..2b2359c38 100644 --- a/packages/integration-tokens/.gitignore +++ b/packages/oauth/.gitignore @@ -2,3 +2,4 @@ /dist .DS_Store .env +*.tgz diff --git a/packages/integration-tokens/.prettierignore b/packages/oauth/.prettierignore similarity index 100% rename from packages/integration-tokens/.prettierignore rename to packages/oauth/.prettierignore diff --git a/packages/integration-oauth/CHANGELOG.md b/packages/oauth/CHANGELOG.md similarity index 100% rename from packages/integration-oauth/CHANGELOG.md rename to packages/oauth/CHANGELOG.md diff --git a/packages/integration-oauth/README.md b/packages/oauth/README.md similarity index 100% rename from packages/integration-oauth/README.md rename to packages/oauth/README.md diff --git a/packages/oauth/package.json b/packages/oauth/package.json new file mode 100644 index 000000000..ecd5dc9a6 --- /dev/null +++ b/packages/oauth/package.json @@ -0,0 +1,48 @@ +{ + "name": "@lucia-auth/oauth", + "version": "1.1.1", + "description": "OAuth integration for Lucia", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "module": "dist/index.js", + "type": "module", + "files": [ + "/dist/", + "CHANGELOG.md" + ], + "scripts": { + "build": "shx rm -rf ./dist/* && tsc", + "auri.publish": "pnpm build && pnpm publish --no-git-checks --access public" + }, + "keywords": [ + "lucia", + "lucia", + "authentication", + "auth", + "oauth" + ], + "repository": { + "type": "git", + "url": "https://github.com/pilcrowOnPaper/lucia", + "directory": "packages/oauth" + }, + "author": "pilcrowonpaper", + "license": "MIT", + "exports": { + ".": "./dist/index.js", + "./providers": "./dist/providers/index.js" + }, + "typesVersions": { + "*": { + "providers": [ + "dist/providers/index.d.ts" + ] + } + }, + "devDependencies": { + "lucia": "latest" + }, + "peerDependencies": { + "lucia": "^2.0.0" + } +} diff --git a/packages/oauth/src/ambient.d.ts b/packages/oauth/src/ambient.d.ts new file mode 100644 index 000000000..97a71b7b4 --- /dev/null +++ b/packages/oauth/src/ambient.d.ts @@ -0,0 +1,6 @@ +/// +declare namespace Lucia { + type Auth = any; + type DatabaseUserAttributes = {}; + type DatabaseSessionAttributes = {}; +} diff --git a/packages/integration-oauth/src/core.ts b/packages/oauth/src/core.ts similarity index 57% rename from packages/integration-oauth/src/core.ts rename to packages/oauth/src/core.ts index 13cd917e4..6549f057d 100644 --- a/packages/integration-oauth/src/core.ts +++ b/packages/oauth/src/core.ts @@ -1,54 +1,8 @@ -import { generateRandomString } from "lucia-auth"; +import { generateRandomString } from "lucia/utils"; -import type { Auth, Key, LuciaError } from "lucia-auth"; +import type { Auth, Key, LuciaError } from "lucia"; import type { CreateUserAttributesParameter, LuciaUser } from "./lucia.js"; -// deprecate in v2 for better api -export const provider = < - _Auth extends Auth, - _ProviderUser extends {}, - _Tokens extends { - accessToken: string; - } ->( - auth: _Auth, - config: { - providerId: string; - getAuthorizationUrl: (state: string) => Promise; - getTokens: (code: string) => Promise<_Tokens>; - getProviderUser: ( - accessToken: string - ) => Promise< - readonly [providerUserId: string, providerUser: _ProviderUser] - >; - } -) => { - return { - getAuthorizationUrl: async () => { - const state = generateState(); - const url = await config.getAuthorizationUrl(state); - return [url, state] as const; - }, - validateCallback: async (code: string) => { - const tokens = await config.getTokens(code); - const [providerUserId, providerUser] = await config.getProviderUser( - tokens.accessToken - ); - const providerAuth = await connectAuth( - auth, - config.providerId, - providerUserId - ); - return { - ...providerAuth, - providerUser: providerUser as _ProviderUser, - providerUserId, - tokens - } as const; - } - } as const satisfies OAuthProvider<_Auth>; -}; - export type OAuthConfig = { clientId: string; clientSecret: string; @@ -64,7 +18,7 @@ export type OAuthProvider = { createUser: ( attributes: CreateUserAttributesParameter ) => Promise>; - createPersistentKey: (userId: string) => Promise; + createKey: (userId: string) => Promise; providerUser: Record; tokens: { accessToken: string; @@ -93,7 +47,7 @@ export const scope = (base: string[], config: string[] = []) => { return [...base, ...(config ?? [])].join(" "); }; -export const connectAuth = async <_Auth extends Auth>( +export const useAuth = async <_Auth extends Auth>( auth: _Auth, providerId: string, providerUserId: string @@ -112,9 +66,8 @@ export const connectAuth = async <_Auth extends Auth>( const existingUser = await getExistingUser(); return { existingUser, - createPersistentKey: async (userId: string) => { + createKey: async (userId: string) => { return await auth.createKey(userId, { - type: "persistent", providerId: providerId, providerUserId, password: null @@ -124,7 +77,7 @@ export const connectAuth = async <_Auth extends Auth>( attributes: CreateUserAttributesParameter<_Auth> ): Promise> => { const user = await auth.createUser({ - primaryKey: { + key: { providerId: providerId, providerUserId, password: null diff --git a/packages/oauth/src/index.ts b/packages/oauth/src/index.ts new file mode 100644 index 000000000..237d7ed89 --- /dev/null +++ b/packages/oauth/src/index.ts @@ -0,0 +1,3 @@ +export { generateState, LuciaOAuthRequestError, useAuth } from "./core.js"; + +export type { OAuthProvider } from "./core.js"; diff --git a/packages/integration-oauth/src/lucia.ts b/packages/oauth/src/lucia.ts similarity index 85% rename from packages/integration-oauth/src/lucia.ts rename to packages/oauth/src/lucia.ts index 7293725e5..9d68bece4 100644 --- a/packages/integration-oauth/src/lucia.ts +++ b/packages/oauth/src/lucia.ts @@ -1,4 +1,4 @@ -import type { Auth } from "lucia-auth"; +import type { Auth } from "lucia"; import type { AwaitedReturnType } from "./utils.js"; export type LuciaUser = AwaitedReturnType; diff --git a/packages/integration-oauth/src/providers/auth0.ts b/packages/oauth/src/providers/auth0.ts similarity index 89% rename from packages/integration-oauth/src/providers/auth0.ts rename to packages/oauth/src/providers/auth0.ts index 496414210..f5cef4fff 100644 --- a/packages/integration-oauth/src/providers/auth0.ts +++ b/packages/oauth/src/providers/auth0.ts @@ -1,7 +1,7 @@ import { createUrl, handleRequest, authorizationHeaders } from "../request.js"; -import { scope, generateState, connectAuth } from "../core.js"; +import { scope, generateState, useAuth } from "../core.js"; -import type { Auth } from "lucia-auth"; +import type { Auth } from "lucia"; import type { OAuthConfig, OAuthProvider } from "../core.js"; const PROVIDER_ID = "auth0"; @@ -64,14 +64,14 @@ export const auth0 = <_Auth extends Auth>(auth: _Auth, config: Config) => { }; return { - getAuthorizationUrl: async (redirectUri?: string) => { + getAuthorizationUrl: async () => { const state = generateState(); const url = createUrl( new URL("/authorize", config.appDomain).toString(), { client_id: config.clientId, response_type: "code", - redirect_uri: redirectUri ?? config.redirectUri, + redirect_uri: config.redirectUri, scope: scope(["openid", "profile"], config.scope), state, ...(config.connection && { connection: config.connection }), @@ -86,9 +86,13 @@ export const auth0 = <_Auth extends Auth>(auth: _Auth, config: Config) => { const tokens = await getTokens(code); const providerUser = await getProviderUser(tokens.accessToken); const providerUserId = providerUser.id; - const providerAuth = await connectAuth(auth, PROVIDER_ID, providerUserId); + const providerAuthHelpersHelpers = await useAuth( + auth, + PROVIDER_ID, + providerUserId + ); return { - ...providerAuth, + ...providerAuthHelpersHelpers, providerUser, tokens }; diff --git a/packages/integration-oauth/src/providers/discord.ts b/packages/oauth/src/providers/discord.ts similarity index 87% rename from packages/integration-oauth/src/providers/discord.ts rename to packages/oauth/src/providers/discord.ts index b937247eb..5a090d75e 100644 --- a/packages/integration-oauth/src/providers/discord.ts +++ b/packages/oauth/src/providers/discord.ts @@ -1,7 +1,7 @@ import { createUrl, handleRequest, authorizationHeaders } from "../request.js"; -import { connectAuth, generateState, scope } from "../core.js"; +import { useAuth, generateState, scope } from "../core.js"; -import type { Auth } from "lucia-auth"; +import type { Auth } from "lucia"; import type { OAuthConfig, OAuthProvider } from "../core.js"; type Config = OAuthConfig & { @@ -48,13 +48,13 @@ export const discord = <_Auth extends Auth>(auth: _Auth, config: Config) => { }; return { - getAuthorizationUrl: async (redirectUri?: string) => { + getAuthorizationUrl: async () => { const state = generateState(); const url = createUrl("https://discord.com/oauth2/authorize", { response_type: "code", client_id: config.clientId, scope: scope(["identify"], config.scope), - redirect_uri: redirectUri ?? config.redirectUri, + redirect_uri: config.redirectUri, state }); return [url, state]; @@ -63,9 +63,13 @@ export const discord = <_Auth extends Auth>(auth: _Auth, config: Config) => { const tokens = await getTokens(code); const providerUser = await getProviderUser(tokens.accessToken); const providerUserId = providerUser.id; - const providerAuth = await connectAuth(auth, PROVIDER_ID, providerUserId); + const providerAuthHelpers = await useAuth( + auth, + PROVIDER_ID, + providerUserId + ); return { - ...providerAuth, + ...providerAuthHelpers, providerUser, tokens }; diff --git a/packages/integration-oauth/src/providers/facebook.ts b/packages/oauth/src/providers/facebook.ts similarity index 85% rename from packages/integration-oauth/src/providers/facebook.ts rename to packages/oauth/src/providers/facebook.ts index 93f7d4587..75362b60d 100644 --- a/packages/integration-oauth/src/providers/facebook.ts +++ b/packages/oauth/src/providers/facebook.ts @@ -1,7 +1,7 @@ import { createUrl, handleRequest, authorizationHeaders } from "../request.js"; -import { scope, generateState, connectAuth } from "../core.js"; +import { scope, generateState, useAuth } from "../core.js"; -import type { Auth } from "lucia-auth"; +import type { Auth } from "lucia"; import type { OAuthConfig, OAuthProvider } from "../core.js"; type Config = OAuthConfig & { @@ -48,12 +48,12 @@ export const facebook = <_Auth extends Auth>(auth: _Auth, config: Config) => { }; return { - getAuthorizationUrl: async (redirectUri?: string) => { + getAuthorizationUrl: async () => { const state = generateState(); const url = createUrl("https://www.facebook.com/v16.0/dialog/oauth", { client_id: config.clientId, scope: scope([], config.scope), - redirect_uri: redirectUri ?? config.redirectUri, + redirect_uri: config.redirectUri, state }); return [url, state] as const; @@ -62,9 +62,13 @@ export const facebook = <_Auth extends Auth>(auth: _Auth, config: Config) => { const tokens = await getTokens(code); const providerUser = await getProviderUser(tokens.accessToken); const providerUserId = providerUser.id; - const providerAuth = await connectAuth(auth, PROVIDER_ID, providerUserId); + const providerAuthHelpers = await useAuth( + auth, + PROVIDER_ID, + providerUserId + ); return { - ...providerAuth, + ...providerAuthHelpers, providerUser, tokens }; diff --git a/packages/integration-oauth/src/providers/github.ts b/packages/oauth/src/providers/github.ts similarity index 88% rename from packages/integration-oauth/src/providers/github.ts rename to packages/oauth/src/providers/github.ts index ee826daac..a2a43e48f 100644 --- a/packages/integration-oauth/src/providers/github.ts +++ b/packages/oauth/src/providers/github.ts @@ -1,7 +1,7 @@ import { createUrl, handleRequest, authorizationHeaders } from "../request.js"; -import { scope, provider, generateState, connectAuth } from "../core.js"; +import { scope, generateState, useAuth } from "../core.js"; -import type { Auth } from "lucia-auth"; +import type { Auth } from "lucia"; import type { OAuthConfig, OAuthProvider } from "../core.js"; const PROVIDER_ID = "github"; @@ -72,18 +72,15 @@ export const github = <_Auth extends Auth>(auth: _Auth, config: Config) => { }; return { - getAuthorizationUrl: async (redirectUri?: string) => { + getAuthorizationUrl: async () => { const state = generateState(); const url = createUrl("https://github.com/login/oauth/authorize", { client_id: config.clientId, scope: scope([], config.scope), state }); - if (config.redirectUri != undefined || redirectUri != undefined) { - url.searchParams.set( - "redirect_uri", - redirectUri ?? (config.redirectUri as string) - ); + if (config.redirectUri) { + url.searchParams.set("redirect_uri", config.redirectUri); } return [url, state] as const; }, @@ -91,9 +88,13 @@ export const github = <_Auth extends Auth>(auth: _Auth, config: Config) => { const tokens = await getTokens(code); const providerUser = await getProviderUser(tokens.accessToken); const providerUserId = providerUser.id.toString(); - const providerAuth = await connectAuth(auth, PROVIDER_ID, providerUserId); + const providerAuthHelpers = await useAuth( + auth, + PROVIDER_ID, + providerUserId + ); return { - ...providerAuth, + ...providerAuthHelpers, providerUser, tokens }; diff --git a/packages/integration-oauth/src/providers/google.ts b/packages/oauth/src/providers/google.ts similarity index 80% rename from packages/integration-oauth/src/providers/google.ts rename to packages/oauth/src/providers/google.ts index 4f3bd9d53..b1203db6d 100644 --- a/packages/integration-oauth/src/providers/google.ts +++ b/packages/oauth/src/providers/google.ts @@ -1,12 +1,11 @@ -import { connectAuth, generateState, scope } from "../core.js"; -import { authorizationHeaders, createUrl, handleRequest } from "../request.js"; +import { createUrl, handleRequest, authorizationHeaders } from "../request.js"; +import { scope, generateState, useAuth } from "../core.js"; -import type { Auth } from "lucia-auth"; +import type { Auth } from "lucia"; import type { OAuthConfig, OAuthProvider } from "../core.js"; type Config = OAuthConfig & { redirectUri: string; - accessType?: "online" | "offline"; }; const PROVIDER_ID = "google"; @@ -48,17 +47,16 @@ export const google = <_Auth extends Auth>(auth: _Auth, config: Config) => { }; return { - getAuthorizationUrl: async (redirectUri?: string) => { + getAuthorizationUrl: async () => { const state = generateState(); const url = createUrl("https://accounts.google.com/o/oauth2/v2/auth", { client_id: config.clientId, - redirect_uri: redirectUri ?? config.redirectUri, + redirect_uri: config.redirectUri, scope: scope( ["https://www.googleapis.com/auth/userinfo.profile"], config.scope ), response_type: "code", - access_type: config.accessType ?? "online", state }); return [url, state] as const; @@ -67,9 +65,13 @@ export const google = <_Auth extends Auth>(auth: _Auth, config: Config) => { const tokens = await getTokens(code); const providerUser = await getProviderUser(tokens.accessToken); const providerUserId = providerUser.sub; - const providerAuth = await connectAuth(auth, PROVIDER_ID, providerUserId); + const providerAuthHelpers = await useAuth( + auth, + PROVIDER_ID, + providerUserId + ); return { - ...providerAuth, + ...providerAuthHelpers, providerUser, tokens }; @@ -80,8 +82,11 @@ export const google = <_Auth extends Auth>(auth: _Auth, config: Config) => { export type GoogleUser = { sub: string; name: string; + given_name: string; + family_name: string; picture: string; - email?: string; + email: string; email_verified: boolean; locale: string; + hd: string; }; diff --git a/packages/integration-oauth/src/providers/index.ts b/packages/oauth/src/providers/index.ts similarity index 100% rename from packages/integration-oauth/src/providers/index.ts rename to packages/oauth/src/providers/index.ts diff --git a/packages/integration-oauth/src/providers/linkedin.ts b/packages/oauth/src/providers/linkedin.ts similarity index 90% rename from packages/integration-oauth/src/providers/linkedin.ts rename to packages/oauth/src/providers/linkedin.ts index c9c9115de..39ee2817a 100644 --- a/packages/integration-oauth/src/providers/linkedin.ts +++ b/packages/oauth/src/providers/linkedin.ts @@ -1,7 +1,7 @@ import { createUrl, handleRequest, authorizationHeaders } from "../request.js"; -import { scope, generateState, connectAuth } from "../core.js"; +import { scope, generateState, useAuth } from "../core.js"; -import type { Auth } from "lucia-auth"; +import type { Auth } from "lucia"; import type { OAuthConfig, OAuthProvider } from "../core.js"; const PROVIDER_ID = "linkedin"; @@ -78,12 +78,12 @@ export const linkedin = <_Auth extends Auth>(auth: _Auth, config: Config) => { }; return { - getAuthorizationUrl: async (redirectUri?: string) => { + getAuthorizationUrl: async () => { const state = generateState(); const url = createUrl("https://www.linkedin.com/oauth/v2/authorization", { client_id: config.clientId, response_type: "code", - redirect_uri: redirectUri ?? config.redirectUri, + redirect_uri: config.redirectUri, scope: scope(["r_liteprofile"], config.scope), state }); @@ -93,9 +93,13 @@ export const linkedin = <_Auth extends Auth>(auth: _Auth, config: Config) => { const tokens = await getTokens(code); const providerUser = await getProviderUser(tokens.accessToken); const providerUserId = providerUser.id; - const providerAuth = await connectAuth(auth, PROVIDER_ID, providerUserId); + const providerAuthHelpers = await useAuth( + auth, + PROVIDER_ID, + providerUserId + ); return { - ...providerAuth, + ...providerAuthHelpers, providerUser, tokens }; diff --git a/packages/integration-oauth/src/providers/patreon.ts b/packages/oauth/src/providers/patreon.ts similarity index 87% rename from packages/integration-oauth/src/providers/patreon.ts rename to packages/oauth/src/providers/patreon.ts index a0ddfe61f..b7bc148ab 100644 --- a/packages/integration-oauth/src/providers/patreon.ts +++ b/packages/oauth/src/providers/patreon.ts @@ -1,7 +1,7 @@ import { createUrl, handleRequest, authorizationHeaders } from "../request.js"; -import { scope, generateState, connectAuth } from "../core.js"; +import { scope, generateState, useAuth } from "../core.js"; -import type { Auth } from "lucia-auth"; +import type { Auth } from "lucia"; import type { OAuthConfig, OAuthProvider } from "../core.js"; type Config = OAuthConfig & { @@ -56,11 +56,11 @@ export const patreon = <_Auth extends Auth>(auth: _Auth, config: Config) => { }; return { - getAuthorizationUrl: async (redirectUri?: string) => { + getAuthorizationUrl: async () => { const state = generateState(); const url = createUrl("https://www.patreon.com/oauth2/authorize", { client_id: config.clientId, - redirect_uri: redirectUri ?? config.redirectUri, + redirect_uri: config.redirectUri, scope: scope(["identity"], config.scope), response_type: "code", state @@ -71,9 +71,13 @@ export const patreon = <_Auth extends Auth>(auth: _Auth, config: Config) => { const tokens = await getTokens(code); const providerUser = await getProviderUser(tokens.accessToken); const providerUserId = providerUser.id; - const providerAuth = await connectAuth(auth, PROVIDER_ID, providerUserId); + const providerAuthHelpers = await useAuth( + auth, + PROVIDER_ID, + providerUserId + ); return { - ...providerAuth, + ...providerAuthHelpers, providerUser, tokens }; diff --git a/packages/integration-oauth/src/providers/reddit.ts b/packages/oauth/src/providers/reddit.ts similarity index 95% rename from packages/integration-oauth/src/providers/reddit.ts rename to packages/oauth/src/providers/reddit.ts index 9a5777255..ebd2ac8a4 100644 --- a/packages/integration-oauth/src/providers/reddit.ts +++ b/packages/oauth/src/providers/reddit.ts @@ -1,7 +1,7 @@ import { createUrl, handleRequest, authorizationHeaders } from "../request.js"; -import { scope, generateState, connectAuth } from "../core.js"; +import { scope, generateState, useAuth } from "../core.js"; -import type { Auth } from "lucia-auth"; +import type { Auth } from "lucia"; import type { OAuthConfig, OAuthProvider } from "../core.js"; type Config = OAuthConfig & { @@ -43,12 +43,12 @@ export const reddit = <_Auth extends Auth>(auth: _Auth, config: Config) => { }; return { - getAuthorizationUrl: async (redirectUri?: string) => { + getAuthorizationUrl: async () => { const state = generateState(); const url = createUrl("https://www.reddit.com/api/v1/authorize", { client_id: config.clientId, response_type: "code", - redirect_uri: redirectUri ?? config.redirectUri, + redirect_uri: config.redirectUri, duration: "permanent", scope: scope([], config.scope), state @@ -59,9 +59,13 @@ export const reddit = <_Auth extends Auth>(auth: _Auth, config: Config) => { const tokens = await getTokens(code); const providerUser = await getProviderUser(tokens.accessToken); const providerUserId = providerUser.id; - const providerAuth = await connectAuth(auth, PROVIDER_ID, providerUserId); + const providerAuthHelpers = await useAuth( + auth, + PROVIDER_ID, + providerUserId + ); return { - ...providerAuth, + ...providerAuthHelpers, providerUser, tokens }; diff --git a/packages/integration-oauth/src/providers/twitch.ts b/packages/oauth/src/providers/twitch.ts similarity index 88% rename from packages/integration-oauth/src/providers/twitch.ts rename to packages/oauth/src/providers/twitch.ts index 1ee30eaca..ab17b7675 100644 --- a/packages/integration-oauth/src/providers/twitch.ts +++ b/packages/oauth/src/providers/twitch.ts @@ -1,7 +1,7 @@ import { createUrl, handleRequest, authorizationHeaders } from "../request.js"; -import { scope, generateState, connectAuth } from "../core.js"; +import { scope, generateState, useAuth } from "../core.js"; -import type { Auth } from "lucia-auth"; +import type { Auth } from "lucia"; import type { OAuthConfig, OAuthProvider } from "../core.js"; type Config = OAuthConfig & { @@ -51,12 +51,12 @@ export const twitch = <_Auth extends Auth>(auth: _Auth, config: Config) => { }; return { - getAuthorizationUrl: async (redirectUri?: string) => { + getAuthorizationUrl: async () => { const state = generateState(); const forceVerify = config.forceVerify ?? false; const url = createUrl("https://id.twitch.tv/oauth2/authorize", { client_id: config.clientId, - redirect_uri: redirectUri ?? config.redirectUri, + redirect_uri: config.redirectUri, scope: scope([], config.scope), response_type: "code", force_verify: forceVerify.toString(), @@ -68,9 +68,13 @@ export const twitch = <_Auth extends Auth>(auth: _Auth, config: Config) => { const tokens = await getTokens(code); const providerUser = await getProviderUser(tokens.accessToken); const providerUserId = providerUser.id; - const providerAuth = await connectAuth(auth, PROVIDER_ID, providerUserId); + const providerAuthHelpers = await useAuth( + auth, + PROVIDER_ID, + providerUserId + ); return { - ...providerAuth, + ...providerAuthHelpers, providerUser, tokens }; diff --git a/packages/integration-oauth/src/request.ts b/packages/oauth/src/request.ts similarity index 100% rename from packages/integration-oauth/src/request.ts rename to packages/oauth/src/request.ts diff --git a/packages/integration-oauth/src/utils.ts b/packages/oauth/src/utils.ts similarity index 100% rename from packages/integration-oauth/src/utils.ts rename to packages/oauth/src/utils.ts diff --git a/packages/integration-oauth/tsconfig.json b/packages/oauth/tsconfig.json similarity index 100% rename from packages/integration-oauth/tsconfig.json rename to packages/oauth/tsconfig.json diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e2fc3898e..28229fbcb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,4 @@ packages: - - "packages/*/dist" - "packages/*" - "apps/**" - "documentation/**"