From 59f82ede65cb87baff3c3e9a2912a1de8ca70c63 Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Sun, 24 Sep 2023 19:22:49 +0800 Subject: [PATCH] chore: migrate to lucia auth (#2242) - Once https://github.com/redis/ioredis/issues/1822 is fixed, remove patch to ioredis - Once https://github.com/Azure/azure-functions-host/issues/162 is fixed, remove patch to nitro that wraps console log. - Once https://github.com/unjs/nitro/pull/1753 is merged and released, remove corresponding patch to nitro - Once https://github.com/lucia-auth/lucia/issues/1153 is fixed, rename models/fields in prisma - Once https://github.com/lucia-auth/lucia/pull/1155 is merged and released, remove custom h3 lucia middleware - Once https://github.com/lucia-auth/lucia/issues/1074 is fixed, remove lucia types shims - Enable CSRF protection in lucia (but then login doesn't work anymore...) --- .github/workflows/ci.yml | 4 +- ...xpress-session-npm-1.17.3-0819dbe06c.patch | 25 - .../ioredis-npm-5.3.2-58471071b1.patch | 124 +++++ .../nitropack-npm-2.6.3-72f7352600.patch | 114 +++++ .../redis-mock-npm-0.56.3-967bd7c6ea.patch | 15 - package.json | 18 +- server/api.e2e.test.ts | 2 +- server/api/index.ts | 3 + server/context.ts | 76 +-- .../migrations/20230921053824_/migration.sql | 26 + server/database/schema.prisma | 11 +- server/database/seed.ts | 11 +- server/groups/resolvers.ts | 3 +- server/middleware/auth.ts | 11 - server/tsyringe.config.ts | 11 +- server/tsyringe.ts | 8 +- server/user/auth.email.strategy.ts | 38 -- server/user/auth.service.ts | 183 ++++--- server/user/e2e.test.ts | 10 +- server/user/passport-initializer.ts | 111 ----- server/user/resolvers.ts | 45 +- server/utils/luciaMiddleware.ts | 20 + server/utils/services.factory.ts | 119 ++--- shims-lucia.d.ts | 14 + test/apollo.server.ts | 12 +- test/context.helper.ts | 4 +- test/global.setup.ts | 11 +- yarn.lock | 458 ++++++------------ 28 files changed, 703 insertions(+), 784 deletions(-) delete mode 100644 .yarn/patches/express-session-npm-1.17.3-0819dbe06c.patch create mode 100644 .yarn/patches/ioredis-npm-5.3.2-58471071b1.patch create mode 100644 .yarn/patches/nitropack-npm-2.6.3-72f7352600.patch delete mode 100644 .yarn/patches/redis-mock-npm-0.56.3-967bd7c6ea.patch create mode 100644 server/database/migrations/20230921053824_/migration.sql delete mode 100644 server/middleware/auth.ts delete mode 100644 server/user/auth.email.strategy.ts delete mode 100644 server/user/passport-initializer.ts create mode 100644 server/utils/luciaMiddleware.ts create mode 100644 shims-lucia.d.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7613133bc..e1e53bc7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3.8.1 with: - node-version: 16 + node-version: 20 cache: 'yarn' - name: Install dependencies @@ -47,7 +47,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node: [16] + node: [20] services: postgres: diff --git a/.yarn/patches/express-session-npm-1.17.3-0819dbe06c.patch b/.yarn/patches/express-session-npm-1.17.3-0819dbe06c.patch deleted file mode 100644 index 332b1340a..000000000 --- a/.yarn/patches/express-session-npm-1.17.3-0819dbe06c.patch +++ /dev/null @@ -1,25 +0,0 @@ -diff --git a/index.js b/index.js -index 40a442baf2fec2c6bdce79a5fba5086b5b8aac07..d4ce261b528b4976cceca5e29d629d7c1f812c43 100644 ---- a/index.js -+++ b/index.js -@@ -273,7 +273,10 @@ function session(options) { - } - - if (!res._header) { -- res._implicitHeader() -+ // CHANGED: Workaround for issue with Azure deploy: https://github.com/unjs/nitro/issues/351 -+ // Original code taken from https://github.com/nodejs/node/blob/main/lib/_http_server.js -+ res.writeHead(res.statusCode) -+ // res._implicitHeader() - } - - if (chunk == null) { -@@ -663,6 +666,8 @@ function setcookie(res, name, val, secret, options) { - - res.setHeader('Set-Cookie', header) - } -+// CHANGED: Since we need this function for an ugly workaround -+exports.setcookie = setcookie; - - /** - * Verify and decode the given `val` with `secrets`. diff --git a/.yarn/patches/ioredis-npm-5.3.2-58471071b1.patch b/.yarn/patches/ioredis-npm-5.3.2-58471071b1.patch new file mode 100644 index 000000000..9f016b639 --- /dev/null +++ b/.yarn/patches/ioredis-npm-5.3.2-58471071b1.patch @@ -0,0 +1,124 @@ +diff --git a/built/index.js b/built/index.js +index 24a189578794dc3073278788fc443e6c8dc957d5..2152cb839b54377fb4fa423151839bfb5cdaed23 100644 +--- a/built/index.js ++++ b/built/index.js +@@ -2,61 +2,61 @@ + Object.defineProperty(exports, "__esModule", { value: true }); + exports.print = exports.ReplyError = exports.SentinelIterator = exports.SentinelConnector = exports.AbstractConnector = exports.Pipeline = exports.ScanStream = exports.Command = exports.Cluster = exports.Redis = exports.default = void 0; + exports = module.exports = require("./Redis").default; +-var Redis_1 = require("./Redis"); +-Object.defineProperty(exports, "default", { enumerable: true, get: function () { return Redis_1.default; } }); +-var Redis_2 = require("./Redis"); +-Object.defineProperty(exports, "Redis", { enumerable: true, get: function () { return Redis_2.default; } }); +-var cluster_1 = require("./cluster"); +-Object.defineProperty(exports, "Cluster", { enumerable: true, get: function () { return cluster_1.default; } }); +-/** +- * @ignore +- */ +-var Command_1 = require("./Command"); +-Object.defineProperty(exports, "Command", { enumerable: true, get: function () { return Command_1.default; } }); +-/** +- * @ignore +- */ +-var ScanStream_1 = require("./ScanStream"); +-Object.defineProperty(exports, "ScanStream", { enumerable: true, get: function () { return ScanStream_1.default; } }); +-/** +- * @ignore +- */ +-var Pipeline_1 = require("./Pipeline"); +-Object.defineProperty(exports, "Pipeline", { enumerable: true, get: function () { return Pipeline_1.default; } }); +-/** +- * @ignore +- */ +-var AbstractConnector_1 = require("./connectors/AbstractConnector"); +-Object.defineProperty(exports, "AbstractConnector", { enumerable: true, get: function () { return AbstractConnector_1.default; } }); +-/** +- * @ignore +- */ +-var SentinelConnector_1 = require("./connectors/SentinelConnector"); +-Object.defineProperty(exports, "SentinelConnector", { enumerable: true, get: function () { return SentinelConnector_1.default; } }); +-Object.defineProperty(exports, "SentinelIterator", { enumerable: true, get: function () { return SentinelConnector_1.SentinelIterator; } }); +-// No TS typings +-exports.ReplyError = require("redis-errors").ReplyError; +-/** +- * @ignore +- */ +-Object.defineProperty(exports, "Promise", { +- get() { +- console.warn("ioredis v5 does not support plugging third-party Promise library anymore. Native Promise will be used."); +- return Promise; +- }, +- set(_lib) { +- console.warn("ioredis v5 does not support plugging third-party Promise library anymore. Native Promise will be used."); +- }, +-}); +-/** +- * @ignore +- */ +-function print(err, reply) { +- if (err) { +- console.log("Error: " + err); +- } +- else { +- console.log("Reply: " + reply); +- } +-} +-exports.print = print; ++// var Redis_1 = require("./Redis"); ++// Object.defineProperty(exports, "default", { enumerable: true, get: function () { return Redis_1.default; } }); ++// var Redis_2 = require("./Redis"); ++// Object.defineProperty(exports, "Redis", { enumerable: true, get: function () { return Redis_2.default; } }); ++// var cluster_1 = require("./cluster"); ++// Object.defineProperty(exports, "Cluster", { enumerable: true, get: function () { return cluster_1.default; } }); ++// /** ++// * @ignore ++// */ ++// var Command_1 = require("./Command"); ++// Object.defineProperty(exports, "Command", { enumerable: true, get: function () { return Command_1.default; } }); ++// /** ++// * @ignore ++// */ ++// var ScanStream_1 = require("./ScanStream"); ++// Object.defineProperty(exports, "ScanStream", { enumerable: true, get: function () { return ScanStream_1.default; } }); ++// /** ++// * @ignore ++// */ ++// var Pipeline_1 = require("./Pipeline"); ++// Object.defineProperty(exports, "Pipeline", { enumerable: true, get: function () { return Pipeline_1.default; } }); ++// /** ++// * @ignore ++// */ ++// var AbstractConnector_1 = require("./connectors/AbstractConnector"); ++// Object.defineProperty(exports, "AbstractConnector", { enumerable: true, get: function () { return AbstractConnector_1.default; } }); ++// /** ++// * @ignore ++// */ ++// var SentinelConnector_1 = require("./connectors/SentinelConnector"); ++// Object.defineProperty(exports, "SentinelConnector", { enumerable: true, get: function () { return SentinelConnector_1.default; } }); ++// Object.defineProperty(exports, "SentinelIterator", { enumerable: true, get: function () { return SentinelConnector_1.SentinelIterator; } }); ++// // No TS typings ++// exports.ReplyError = require("redis-errors").ReplyError; ++// /** ++// * @ignore ++// */ ++// Object.defineProperty(exports, "Promise", { ++// get() { ++// console.warn("ioredis v5 does not support plugging third-party Promise library anymore. Native Promise will be used."); ++// return Promise; ++// }, ++// set(_lib) { ++// console.warn("ioredis v5 does not support plugging third-party Promise library anymore. Native Promise will be used."); ++// }, ++// }); ++// /** ++// * @ignore ++// */ ++// function print(err, reply) { ++// if (err) { ++// console.log("Error: " + err); ++// } ++// else { ++// console.log("Reply: " + reply); ++// } ++// } ++// exports.print = print; diff --git a/.yarn/patches/nitropack-npm-2.6.3-72f7352600.patch b/.yarn/patches/nitropack-npm-2.6.3-72f7352600.patch new file mode 100644 index 000000000..3116acbe9 --- /dev/null +++ b/.yarn/patches/nitropack-npm-2.6.3-72f7352600.patch @@ -0,0 +1,114 @@ +diff --git a/dist/runtime/entries/azure.mjs b/dist/runtime/entries/azure.mjs +index 2ed72eb8e5f5f6b740d909bb12f62cf761599569..ac3dad70537e585d63c713048c545238a83d0d5f 100644 +--- a/dist/runtime/entries/azure.mjs ++++ b/dist/runtime/entries/azure.mjs +@@ -3,6 +3,8 @@ import { parseURL } from "ufo"; + import { nitroApp } from "../app.mjs"; + import { getAzureParsedCookiesFromHeaders } from "../utils.azure.mjs"; + import { normalizeLambdaOutgoingHeaders } from "../utils.lambda.mjs"; ++import { createConsola } from "consola"; ++ + export async function handle(context, req) { + let url; + if (req.headers["x-ms-original-url"]) { +@@ -11,6 +13,25 @@ export async function handle(context, req) { + } else { + url = "/api/" + (req.params.url || ""); + } ++ const _getLogFn = (level) => { ++ if (level < 1) { ++ return context.log.error; ++ } ++ if (level === 1) { ++ return context.log.warn; ++ } ++ return context.log; ++ } ++ const consola = createConsola({ ++ reporters: [ ++ { ++ log: (logObj) => { ++ _getLogFn(logObj.level)(logObj.args); ++ }, ++ }, ++ ], ++ }); ++ consola.wrapConsole(); + const { body, status, statusText, headers } = await nitroApp.localCall({ + url, + headers: req.headers, +@@ -18,6 +39,7 @@ export async function handle(context, req) { + // https://github.com/Azure/azure-functions-host/issues/293 + body: req.rawBody + }); ++ consola.restoreConsole(); + context.res = { + status, + // cookies https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=typescript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#http-response +diff --git a/dist/runtime/utils.azure.mjs b/dist/runtime/utils.azure.mjs +index d0e27bd1b36683f7834ed097c07702429da3c152..0dd0309f7c73ccb709c7bee85360267f7198a4da 100644 +--- a/dist/runtime/utils.azure.mjs ++++ b/dist/runtime/utils.azure.mjs +@@ -1,13 +1,56 @@ + import { parse } from "cookie-es"; + import { splitCookiesString } from "h3"; +-import { joinHeaders } from "./utils.mjs"; + export function getAzureParsedCookiesFromHeaders(headers) { +- const c = String(headers["set-cookie"]); +- if (!c || c.length === 0) { ++ const raw = headers["set-cookie"]; ++ if (!raw || typeof raw === "number" || raw.length === 0) { + return []; + } +- const cookies = splitCookiesString(joinHeaders(c)).map( +- (cookie) => parse(cookie) +- ); ++ const rawCookies = Array.isArray(raw) ? raw : splitCookiesString(String(raw)); ++ const cookies = rawCookies.flatMap((cookie) => { ++ const entries = Object.entries(parse(cookie)); ++ if (entries.length > 0) { ++ const [entry, ...rest] = entries; ++ const options = Object.fromEntries( ++ rest.map(([k, v]) => [k.toLowerCase(), v]) ++ ); ++ const res = { ++ name: entry[0], ++ value: entry[1], ++ domain: options.domain, ++ path: options.path, ++ expires: parseNumberOrDate(options.expires), ++ // secure: options.secure, ++ // httponly: options.httponly, ++ samesite: options.samesite, ++ maxAge: parseNumber(options.maxAge) ++ }; ++ for (const key in res) { ++ if (res[key] === void 0) { ++ delete res[key]; ++ } ++ } ++ return [res]; ++ } ++ return []; ++ }); + return cookies; + } ++function parseNumberOrDate(expires) { ++ const expiresAsNumber = parseNumber(expires); ++ if (expiresAsNumber !== void 0) { ++ return expiresAsNumber; ++ } ++ const expiresAsDate = new Date(expires); ++ if (!Number.isNaN(expiresAsDate.getTime())) { ++ return expiresAsDate; ++ } ++} ++function parseNumber(maxAge) { ++ if (!maxAge) { ++ return void 0; ++ } ++ const maxAgeAsNumber = Number(maxAge); ++ if (!Number.isNaN(maxAgeAsNumber)) { ++ return maxAgeAsNumber; ++ } ++} diff --git a/.yarn/patches/redis-mock-npm-0.56.3-967bd7c6ea.patch b/.yarn/patches/redis-mock-npm-0.56.3-967bd7c6ea.patch deleted file mode 100644 index b022ec7a1..000000000 --- a/.yarn/patches/redis-mock-npm-0.56.3-967bd7c6ea.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/lib/server/redis-db.js b/lib/server/redis-db.js -index 35583cc92b5d0b26ac9e8373a2808b133c188552..2eea5dfa3b11ca76891dc11cd8e936533fa1d28b 100644 ---- a/lib/server/redis-db.js -+++ b/lib/server/redis-db.js -@@ -28,7 +28,8 @@ class RedisDb { - * - * The server contains a log of logic. It only feels natural to split it into multiple files - */ --['./strings', './keys', './hash', './set', './list.js', './sortedset'] -- .forEach((lib) => Object.assign(RedisDb.prototype, require(lib))); -+// CHANGED: Don't need those, and they throw module not found errors -+//['./strings', './keys', './hash', './set', './list.js', './sortedset'] -+// .forEach((lib) => Object.assign(RedisDb.prototype, require(lib))); - - module.exports = RedisDb; diff --git a/package.json b/package.json index bb16e4def..81cc14fba 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,8 @@ "@as-integrations/h3": "^1.1.6", "@graphql-tools/schema": "^10.0.0", "@he-tree/vue": "^2.5.1", + "@lucia-auth/adapter-prisma": "^3.0.1", + "@lucia-auth/adapter-session-unstorage": "^2.1.0", "@nozomuikuta/h3-cors": "^0.2.2", "@pinia/nuxt": "^0.4.11", "@popperjs/core": "^2.11.8", @@ -60,23 +62,20 @@ "@yaireo/tagify": "^4.17.9", "autoprefixer": "^10.4.15", "body-scroll-lock": "^4.0.0-beta.0", - "connect-redis": "^7.1.0", "cross-fetch": "^4.0.0", - "express-session": "^1.17.3", "graphql": "^16.8.0", - "graphql-passport": "^0.6.7", "graphql-scalars": "^1.22.2", "json-bigint-patch": "^0.0.8", "lodash": "^4.17.21", + "lucia": "^2.6.0", "nodemailer": "^6.8.0", - "passport": "^0.6.0", "pinia": "^2.1.6", - "redis": "^4.6.8", "reflect-metadata": "^0.1.13", "ts-loader": "^9.4.4", "ts-node": "^10.9.1", "tsyringe": "^4.8.0", "typescript": "^5.2.2", + "unstorage": "^1.9.0", "vee-validate": "^4.11.6", "zod": "^3.22.2" }, @@ -104,12 +103,8 @@ "@tailwindcss/line-clamp": "^0.4.4", "@tailwindcss/typography": "^0.5.10", "@types/bcryptjs": "^2.4.3", - "@types/connect-redis": "^0.0.21", - "@types/express-session": "^1.17.7", "@types/lodash": "^4.14.198", "@types/nodemailer": "^6.4.7", - "@types/passport": "^1.0.12", - "@types/redis-mock": "^0.17.1", "@types/supertest": "^2.0.12", "@types/uuid": "^9.0.4", "@types/yaireo__tagify": "^4.17.2", @@ -144,7 +139,6 @@ "prettier": "^3.0.3", "prettier-plugin-organize-imports": "^3.2.3", "prisma": "^5.3.1", - "redis-mock": "^0.56.3", "storybook": "7.0.26", "storybook-vue-addon": "^0.4.0", "supertest": "^6.3.3", @@ -158,10 +152,10 @@ }, "resolutions": { "@types/react": "https://registry.yarnpkg.com/@favware/skip-dependency/-/skip-dependency-1.2.0.tgz", - "express-session": "patch:express-session@npm%3A1.17.3#./.yarn/patches/express-session-npm-1.17.3-0819dbe06c.patch", "mount-vue-component": "patch:mount-vue-component@npm%3A0.10.2#./.yarn/patches/mount-vue-component-npm-0.10.2-4968f76fd9.patch", "@vue/apollo-util": "patch:@vue/apollo-util@npm%3A4.0.0-beta.6#./.yarn/patches/@vue-apollo-util-npm-4.0.0-beta.6-7e26e14eb7.patch", - "redis-mock@^0.56.3": "patch:redis-mock@npm%3A0.56.3#./.yarn/patches/redis-mock-npm-0.56.3-967bd7c6ea.patch" + "ioredis@^5.3.2": "patch:ioredis@npm%3A5.3.2#./.yarn/patches/ioredis-npm-5.3.2-58471071b1.patch", + "nitropack@^2.6.3": "patch:nitropack@npm%3A2.6.3#./.yarn/patches/nitropack-npm-2.6.3-72f7352600.patch" }, "resolutionsComments": { "@types/react": "Otherwise these types interfere with the types from vite: https://github.com/johnsoncodehk/volar/discussions/592#discussioncomment-1580518" diff --git a/server/api.e2e.test.ts b/server/api.e2e.test.ts index 8c399e318..a04260422 100644 --- a/server/api.e2e.test.ts +++ b/server/api.e2e.test.ts @@ -34,10 +34,10 @@ describe('request without query', () => { .get('/api') .set('Apollo-Require-Preflight', 'True') - expect(response.statusCode).toBe(400) expect(response.text).toContain( 'GraphQL operations must contain a non-empty `query`', ) + expect(response.statusCode).toBe(400) }) }) diff --git a/server/api/index.ts b/server/api/index.ts index 31c881969..44be21ce0 100644 --- a/server/api/index.ts +++ b/server/api/index.ts @@ -8,6 +8,7 @@ import 'json-bigint-patch' // Needed for bigint support in JSON import 'reflect-metadata' // Needed for tsyringe import { buildContext, Context } from '../context' import { loadSchemaWithResolvers } from '../schema' +import { configure as configureTsyringe } from './../tsyringe.config' // Workaround for issue with Azure deploy: https://github.com/unjs/nitro/issues/351 // Original code taken from https://github.com/nodejs/node/blob/main/lib/_http_outgoing.js @@ -84,6 +85,8 @@ http.IncomingMessage.Readable.prototype.unpipe = function (dest) { } export default defineLazyEventHandler(async () => { + configureTsyringe() + const server = new ApolloServer({ schema: await loadSchemaWithResolvers(), introspection: true, diff --git a/server/context.ts b/server/context.ts index fbcc0351e..040e84291 100644 --- a/server/context.ts +++ b/server/context.ts @@ -1,70 +1,32 @@ import { H3ContextFunctionArgument } from '@as-integrations/h3' import { User } from '@prisma/client' -import { - AuthenticateReturn, - buildContext as passportBuildContext, -} from 'graphql-passport' +import { Session } from 'lucia' +import { resolve } from '~/server/tsyringe' export interface Context { - isAuthenticated: () => boolean - isUnauthenticated: () => boolean - getUser: () => User | null - authenticate: ( - strategyName: string, - options?: Record, - ) => Promise> - login: (user: User, options?: Record) => Promise - logout: () => void + /** + * Returns the currently logged in user or null if no user is logged in. + */ + getUser: () => Promise + /** + * Writes the given session to the response (e.g. sets the session cookie). + * If the session is null, the session information is removed from the response, which effectively logs the user out. + */ + setSession: (session: Session | null) => void } export function buildContext({ event, }: H3ContextFunctionArgument): Promise { + const authHandler = resolve('AuthService').createAuthContext(event) return Promise.resolve({ - // @ts-expect-error: h3 doesn't provide correct types https://github.com/unjs/h3/issues/146 - ...passportBuildContext({ req: event.req, res: event.res }), - // The login method provided by graphql-passport doesn't work on azure, so we have to override it - login: async (user) => { - // @ts-expect-error: there are no correct types for this - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const session = event.req.session - if (!session) { - throw new Error( - 'Login sessions require session support. Did you forget to use `express-session` middleware?', - ) - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - session.passport = session.passport || {} - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - session.passport.user = user.id - return new Promise((resolve, reject) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call - session.save(function (err: any) { - if (err) { - reject(err) - return - } - // For some strange reason the session cookie is not set correctly on azure, so do this manually - // const signed = - // 's:' + - // signature.sign( - // event.req.sessionID, - // useRuntimeConfig().session.primarySecret, - // ) - // const data = cookie.serialize('session', signed, session.cookie.data) - // setCookie(event, 'session', data) - // expressSession.setcookie( - // event.res, - // 'session', - // // @ts-expect-error: there are no correct types for this - // event.req.sessionID, - // useRuntimeConfig().session.primarySecret, - // // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - // session.cookie.data, - // ) - resolve() - }) - }) + getUser: async () => { + // Validate internally caches the result, so we don't need to cache it here + const session = await authHandler.validate() + return session?.user ?? null + }, + setSession: (session) => { + authHandler.setSession(session) }, }) } diff --git a/server/database/migrations/20230921053824_/migration.sql b/server/database/migrations/20230921053824_/migration.sql new file mode 100644 index 000000000..3b3c03638 --- /dev/null +++ b/server/database/migrations/20230921053824_/migration.sql @@ -0,0 +1,26 @@ +/* + Warnings: + + - You are about to drop the column `password` on the `User` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "User" DROP COLUMN "password"; + +-- CreateTable +CREATE TABLE "Key" ( + "id" TEXT NOT NULL, + "hashed_password" TEXT, + "user_id" TEXT NOT NULL, + + CONSTRAINT "Key_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Key_id_key" ON "Key"("id"); + +-- CreateIndex +CREATE INDEX "Key_user_id_idx" ON "Key"("user_id"); + +-- AddForeignKey +ALTER TABLE "Key" ADD CONSTRAINT "Key_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/database/schema.prisma b/server/database/schema.prisma index 23a0f5b8e..a3320f1cb 100644 --- a/server/database/schema.prisma +++ b/server/database/schema.prisma @@ -14,10 +14,19 @@ model User { email String @unique emailIsVerified Boolean @default(false) name String? - password String createdAt DateTime @default(now()) documents UserDocument[] groups Group[] + key Key[] // TODO: Rename to keys +} + +model Key { + id String @id @unique + hashed_password String? // TODO: Rename to hashedPassword + user_id String // TODO: Rename to userId + user User @relation(references: [id], fields: [user_id], onDelete: Cascade) + + @@index([user_id]) } enum DocumentType { diff --git a/server/database/seed.ts b/server/database/seed.ts index 8011a9a49..d63985f6c 100644 --- a/server/database/seed.ts +++ b/server/database/seed.ts @@ -17,9 +17,16 @@ async function seedInternal(prisma: PrismaClientT): Promise { data: { id: 'ckn4oul7100004cv7y3t94n8j', email: 'alice@jabref.org', - password: - '19184d8c1c1e9b483d8347f8da0d53ad92170233100d32c3a0d748725948c28d09a060d7f02962b7b93320c72a2cdd94f21b16b08bf8bd1cba0c5f77afeffddbb24df527c4f16f1fca6eb5480159b56df3d818b4b3c74ead04227a78b3d810b8', // EBNPXY35TYkYXHs name: 'Alice', + key: { + create: [ + { + id: 'email:alice@jabref.org', + hashed_password: + 's2:v0m1wv8ia158m21f:6b60b32e60cf1ed5960afca58f2feb1779e409b3570d46919b21d93e342e532238631f8218c9d7854a69f210830c0d1c0bcd74d0ba2a7a2e5483b676f2b619cb', // EBNPXY35TYkYXHs + }, + ], + }, }, }) diff --git a/server/groups/resolvers.ts b/server/groups/resolvers.ts index 44577a908..5c7ee3940 100644 --- a/server/groups/resolvers.ts +++ b/server/groups/resolvers.ts @@ -107,10 +107,11 @@ export class Mutation { ) } + const user = await context.getUser() return await this.groupService.addGroup({ users: { connect: { - id: context.getUser()?.id, + id: user?.id, }, }, name: group.name, diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts deleted file mode 100644 index cbd633e2e..000000000 --- a/server/middleware/auth.ts +++ /dev/null @@ -1,11 +0,0 @@ -import 'reflect-metadata' // Needed for tsyringe -import { resolve } from './../tsyringe' -import { configure as configureTsyringe } from './../tsyringe.config' - -export default defineLazyEventHandler(async () => { - await configureTsyringe() - - const passportInitializer = resolve('PassportInitializer') - passportInitializer.initialize() - return passportInitializer.createHandler() -}) diff --git a/server/tsyringe.config.ts b/server/tsyringe.config.ts index 57e2786c3..9c1816bc4 100644 --- a/server/tsyringe.config.ts +++ b/server/tsyringe.config.ts @@ -9,28 +9,27 @@ import { JournalService } from './journals/journal.service' import * as JournalResolvers from './journals/resolvers' import { instanceCachingFactory, register } from './tsyringe' import { AuthService } from './user/auth.service' -import PassportInitializer from './user/passport-initializer' import * as UserResolvers from './user/resolvers' import { createRedisClient } from './utils/services.factory' const { PrismaClient } = prisma -export async function configure(): Promise { +export function configure() { const config = useRuntimeConfig() as Config + register('Config', { + useValue: config, + }) // Tools register('PrismaClient', { useFactory: instanceCachingFactory(() => new PrismaClient()), }) register('RedisClient', { - useValue: await createRedisClient(config), + useValue: createRedisClient(config), }) registerClasses() } export function registerClasses(): void { - // Tools - register('PassportInitializer', PassportInitializer) - // Services register('UserDocumentService', UserDocumentService) register('AuthService', AuthService) diff --git a/server/tsyringe.ts b/server/tsyringe.ts index c3c87143c..6bc1a3139 100644 --- a/server/tsyringe.ts +++ b/server/tsyringe.ts @@ -12,6 +12,8 @@ import { inject as tsyringeInject, } from 'tsyringe' import { constructor } from 'tsyringe/dist/typings/types' +import type { Storage } from 'unstorage' +import type { Config } from '~/config' import type * as DocumentResolvers from './documents/resolvers' import type { UserDocumentService } from './documents/user.document.service' import type * as GroupResolvers from './groups/resolvers' @@ -19,9 +21,7 @@ import type { GroupService } from './groups/service' import type { JournalService } from './journals/journal.service' import type * as JournalResolvers from './journals/resolvers' import type { AuthService } from './user/auth.service' -import type PassportInitializer from './user/passport-initializer' import type * as UserResolvers from './user/resolvers' -import { RedisClient } from './utils/services.factory' export { injectable, instanceCachingFactory } from 'tsyringe' @@ -48,10 +48,10 @@ function injectSymbol( } export const InjectionSymbols = { + ...injectSymbol('Config')(), // Tools ...injectSymbol('PrismaClient')(), - ...injectSymbol('RedisClient')(), - ...injectSymbol('PassportInitializer')(), + ...injectSymbol('RedisClient')(), // Services ...injectSymbol('UserDocumentService')(), ...injectSymbol('AuthService')(), diff --git a/server/user/auth.email.strategy.ts b/server/user/auth.email.strategy.ts deleted file mode 100644 index 336ab2bc3..000000000 --- a/server/user/auth.email.strategy.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { User } from '@prisma/client' -import { GraphQLLocalStrategy } from 'graphql-passport' -import { AuthenticationMessage, AuthService } from './auth.service' - -export default class EmailStrategy extends GraphQLLocalStrategy { - constructor(private authService: AuthService) { - super( - async ( - email: unknown, - password: unknown, - done: ( - error: Error | null, - user?: User | null, - message?: AuthenticationMessage, - ) => void, - ) => { - try { - const user = await this.authService.validateUser( - email as string, - password as string, - ) - if ('user' in user) { - // Authentication succeeded - done(null, await user.user) - } else { - // Wrong email-password combination - done(null, null, { - message: 'Wrong email or password.', - info: false, - }) - } - } catch (err) { - done(err as Error) - } - }, - ) - } -} diff --git a/server/user/auth.service.ts b/server/user/auth.service.ts index e45208f5d..0c002b7b0 100644 --- a/server/user/auth.service.ts +++ b/server/user/auth.service.ts @@ -1,19 +1,22 @@ import type { PrismaClient, User } from '@prisma/client' import { ResolversTypes } from '#graphql/resolver' +import { Auth, lucia as luciaConstructor, LuciaError } from 'lucia' +import type { Storage } from 'unstorage' import { v4 as generateToken } from 'uuid' +import { Environment, type Config } from '~/config' import { hash, verifyHash } from '../utils/crypto' import { resetPasswordTemplate } from '../utils/resetPasswordTemplate' import { sendEmail } from '../utils/sendEmail' -import type { RedisClient } from '../utils/services.factory' import { inject, injectable } from './../tsyringe' -export type { InfoArgument as AuthenticationMessage } from 'graphql-passport' +import { prisma as prismaAdapter } from '@lucia-auth/adapter-prisma' +import { unstorage as unstorageAdapter } from '@lucia-auth/adapter-session-unstorage' +import { H3Event } from 'h3' +import { h3 } from '../utils/luciaMiddleware' -export interface AuthenticateReturn { - user?: User - message?: string -} +const EMAIL_PROVIDER = 'email' +const FORGOT_PASSWORD_PREFIX = 'forgot-password' export type ChangePasswordPayload = ResolversTypes['ChangePasswordPayload'] export type SignupPayload = ResolversTypes['SignupPayload'] @@ -23,38 +26,67 @@ export type LoginPayload = ResolversTypes['LoginPayload'] @injectable() export class AuthService { + lucia: Auth constructor( @inject('PrismaClient') private prisma: PrismaClient, - @inject('RedisClient') private redisClient: RedisClient, - ) {} + @inject('RedisClient') private redisClient: Storage, + @inject('Config') private config: Config, + ) { + this.lucia = luciaConstructor({ + env: + config.public.environment === Environment.Production ? 'PROD' : 'DEV', + middleware: h3(), + adapter: { + user: prismaAdapter(prisma), + session: unstorageAdapter(redisClient), + }, + getUserAttributes: (user: User) => user, + sessionExpiresIn: { + // Session counts as active for 1 day (afterwards it has to be refreshed) + activePeriod: 1000 * 60 * 60 * 24, + // Session completely expires after half a year + idlePeriod: 0.5 * 31556952 * 1000, + }, + sessionCookie: { + attributes: { + // Blocks sending a cookie in a cross-origin request, protects somewhat against CORS attacks + sameSite: 'strict', + }, + }, + // TODO: Enable (once figure out why login then no longer works) + csrfProtection: false, + }) + } async getUsers(): Promise { return await this.prisma.user.findMany() } - async validateUser(email: string, password: string): Promise { - const user = await this.prisma.user.findUnique({ - where: { - email, - }, - }) - if (!user) { - return { - problems: [ - { path: ['email', 'password'], message: 'Wrong email or password' }, - ], + async validateUser(email: string, password: string) { + try { + const key = await this.lucia.useKey( + EMAIL_PROVIDER, + email.toLowerCase(), + password, + ) + const user = await this.getUserById(key.userId) + if (!user) { + throw new Error('User not found although key was valid') } - } else { - const correctPassword = await verifyHash(password, user.password) - if (correctPassword) { - return { user } - } else { + return user + } catch (error) { + if ( + error instanceof LuciaError && + (error.message === 'AUTH_INVALID_KEY_ID' || + error.message === 'AUTH_INVALID_PASSWORD') + ) { return { problems: [ { path: ['email', 'password'], message: 'Wrong email or password' }, ], } } + throw error } } @@ -63,11 +95,12 @@ export class AuthService { if (!user) { return true } - const PREFIX = process.env.PREFIX ?? 'forgot-password' - const key = PREFIX + user.id + const key = FORGOT_PASSWORD_PREFIX + user.id const token = generateToken() const hashedToken = await hash(token) - await this.redisClient.set(key, hashedToken, { EX: 1000 * 60 * 60 * 24 }) // VALID FOR ONE DAY + await this.redisClient.setItem(key, hashedToken, { + EX: 1000 * 60 * 60 * 24, + }) // Valid for 1 day await sendEmail(email, resetPasswordTemplate(user.id, token)) return true } @@ -83,44 +116,44 @@ export class AuthService { async getUserByEmail(email: string): Promise { return await this.prisma.user.findFirst({ where: { - email, + id: email.toLowerCase(), }, }) } - async createAccount(email: string, password: string): Promise { - const existingUser = await this.prisma.user.findFirst({ - where: { - email, - }, - }) - const userWithEmailAlreadyExists = existingUser !== null - if (userWithEmailAlreadyExists) { - return { - problems: [ - { - path: ['email'], - message: `User with email '${email}' already exists.`, - }, - ], + async createAccount(email: string, password: string) { + try { + const user = await this.lucia.createUser({ + key: { + providerId: EMAIL_PROVIDER, + providerUserId: email.toLowerCase(), + password, + }, + // @ts-expect-error: lucia forces us to pass all attributes, but they are actually generated by the database + attributes: { + email: email.toLowerCase(), + }, + }) + return user + } catch (error) { + if ( + error instanceof LuciaError && + error.message === 'AUTH_DUPLICATE_KEY_ID' + ) { + return { + problems: [ + { + path: ['email'], + message: `User with email '${email}' already exists.`, + }, + ], + } } + throw error } - const hashedPassword = await hash(password) - - const user = await this.prisma.user.create({ - data: { - email, - password: hashedPassword, - }, - }) - return { user } } - async updatePassword( - token: string, - id: string, - newPassword: string, - ): Promise { + async updatePassword(token: string, userId: string, newPassword: string) { if (newPassword.length < 6) { return { problems: [ @@ -131,9 +164,8 @@ export class AuthService { ], } } - const PREFIX = process.env.PREFIX ?? 'forgot-password' - const key = PREFIX + id - const hashedToken = await this.redisClient.get(key) + const key = FORGOT_PASSWORD_PREFIX + userId + const hashedToken = await this.redisClient.getItem(key) if (!hashedToken) { return { message: 'Token Expired', @@ -145,17 +177,28 @@ export class AuthService { message: 'Invalid Token', } } + await this.redisClient.removeItem(key) - await this.redisClient.del(key) - const hashedPassword = await hash(newPassword) - const user = await this.prisma.user.update({ - where: { - id, - }, - data: { - password: hashedPassword, - }, - }) + const user = await this.getUserById(userId) + if (!user) { + return { + message: 'User not found', + } + } + await this.lucia.invalidateAllUserSessions(user.id) + await this.lucia.updateKeyPassword(EMAIL_PROVIDER, user.email, newPassword) return { user } } + + async createSession(user: User | string) { + const userId = typeof user === 'string' ? user : user.id + return await this.lucia.createSession({ + userId, + attributes: {}, + }) + } + + createAuthContext(event: H3Event) { + return this.lucia.handleRequest(event) + } } diff --git a/server/user/e2e.test.ts b/server/user/e2e.test.ts index 16be8218f..3ec8b04ea 100644 --- a/server/user/e2e.test.ts +++ b/server/user/e2e.test.ts @@ -13,6 +13,12 @@ describe('mutation', () => { id } } + ... on InputValidationProblem { + problems { + path + message + } + } } } `) @@ -20,11 +26,11 @@ describe('mutation', () => { input: { email: 'alice@jabref.org', password: 'EBNPXY35TYkYXHs' }, }) expect(errors).toEqual(undefined) - // TODO: Check that there is even a session cookie - expect(response.get('set-cookie')).toBeDefined() expect(data).toStrictEqual({ login: { user: { id: 'ckn4oul7100004cv7y3t94n8j' } }, }) + // TODO: Check that there is even a session cookie + expect(response.get('set-cookie')).toBeDefined() }) }) }) diff --git a/server/user/passport-initializer.ts b/server/user/passport-initializer.ts deleted file mode 100644 index 198e4de7f..000000000 --- a/server/user/passport-initializer.ts +++ /dev/null @@ -1,111 +0,0 @@ -import RedisStore from 'connect-redis' -import session from 'express-session' -import { EventHandler } from 'h3' -import passport from 'passport' -import { Environment } from '~/config' -import type { RedisClient } from '../utils/services.factory' -import { inject, injectable } from './../tsyringe' -import EmailStrategy from './auth.email.strategy' -import { AuthService } from './auth.service' - -@injectable() -export default class PassportInitializer { - constructor( - @inject('AuthService') private accountService: AuthService, - @inject('RedisClient') private redisClient: RedisClient, - ) {} - - initialize(): void { - passport.use(new EmailStrategy(this.accountService)) - passport.serializeUser((user, done) => { - this.serializeUser(user, done) - }) - passport.deserializeUser((id, done) => { - this.deserializeUser(id, done) - }) - } - - createHandler(): EventHandler { - const config = useRuntimeConfig() - - // TODO: Use redis store also for development as soon as mock-redis is compatible with redis v4 - let store: session.Store - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison --- bug in nuxt? - if (config.public.environment === Environment.Production) { - store = new RedisStore({ - client: this.redisClient, - disableTouch: true, - }) - } else { - store = new session.MemoryStore() - } - // If the store implements the touch function, the express-session middleware - // essentially makes res.end an asynchronous operation, which is not what h3 expects. - // Therefore, we disable the touch function. - // As a fix we would need https://github.com/expressjs/session/pull/751 and support for callbacks to res.end in h3 - // @ts-expect-error: the idea is to replace the function by something else - store.touch = false - const sessionMiddleware = session({ - store, - // The secret used to sign the session cookie - secret: [config.session.primarySecret, config.session.secondarySecret], - // Don't force session to be saved back to the session store unless it was modified - resave: false, - saveUninitialized: false, - name: 'session', - cookie: { - // Serve secure cookies (requires HTTPS, so only in production) - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison --- bug in nuxt? - secure: config.public.environment === Environment.Production, - // Blocks the access cookie from javascript, preventing XSS attacks - httpOnly: true, - // Blocks sending a cookie in a cross-origin request, protects somewhat against CORS attacks - sameSite: true, - // Expires after half a year - maxAge: 0.5 * 31556952 * 1000, - }, - }) - const passportMiddleware = passport.initialize() - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const passportSessionMiddleware = passport.session() - - return defineEventHandler(async (event) => { - // Add middleware that sends and receives the session ID using cookies - // @ts-expect-error: https://github.com/unjs/h3/issues/146 - await fromNodeMiddleware(sessionMiddleware)(event) - - // Add passport as middleware (this more or less only adds the _passport variable to the request) - // @ts-expect-error: https://github.com/unjs/h3/issues/146 - await fromNodeMiddleware(passportMiddleware)(event) - - // Add middleware that authenticates request based on the current session state (i.e. we alter the request to contain the hydrated user object instead of only the session ID) - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - await fromNodeMiddleware(passportSessionMiddleware)(event) - }) - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private serializeUser(user: any, done: (err: unknown, id?: string) => void) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument - done(null, user.id) - } - - private deserializeUser( - id: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - done: (err: unknown, user?: any) => void, - ) { - this.accountService - .getUserById(id) - .then((user) => { - if (user === null) { - done("account doesn't exist", undefined) - } else { - done(null, user) - } - }) - .catch((error) => { - done(error, null) - }) - } -} diff --git a/server/user/resolvers.ts b/server/user/resolvers.ts index 87d7179b6..3b2b5cd20 100644 --- a/server/user/resolvers.ts +++ b/server/user/resolvers.ts @@ -50,12 +50,12 @@ export class Query { return await this.authService.getUserById(id) } - me( + async me( _root: Record, _args: Record, context: Context, - ): User | null { - return context.getUser() + ): Promise { + return await context.getUser() } } @@ -69,9 +69,13 @@ export class Mutation { { input: { email, password } }: MutationSignupArgs, context: Context, ): Promise { - const newUserPayload = await this.authService.createAccount(email, password) - if ('user' in newUserPayload) void context.login(await newUserPayload.user) - return newUserPayload + const userOrProblems = await this.authService.createAccount(email, password) + if ('problems' in userOrProblems) { + return userOrProblems + } + const session = await this.authService.createSession(userOrProblems) + context.setSession(session) + return { user: userOrProblems } } @validateInput(LoginInputSchema) @@ -80,26 +84,15 @@ export class Mutation { { input: { email, password } }: MutationLoginArgs, context: Context, ): Promise { - const { user, info } = await context.authenticate('graphql-local', { - email, - password, - }) - if (user) { - // Make login persistent by putting it in the session store - await context.login(user) - return { user } - } else { - return { - problems: [ - { - path: ['email'], - message: - (typeof info === 'string' ? info : info?.message) ?? - 'Unknown error while logging in.', - }, - ], - } + const userOrProblems = await this.authService.validateUser(email, password) + if ('problems' in userOrProblems) { + return userOrProblems } + + // Make login persistent by putting it in the session store + const session = await this.authService.createSession(userOrProblems) + context.setSession(session) + return { user: userOrProblems } } logout( @@ -107,7 +100,7 @@ export class Mutation { _args: Record, context: Context, ): LogoutPayload { - context.logout() + context.setSession(null) return { result: true, } diff --git a/server/utils/luciaMiddleware.ts b/server/utils/luciaMiddleware.ts new file mode 100644 index 000000000..e5e3378d5 --- /dev/null +++ b/server/utils/luciaMiddleware.ts @@ -0,0 +1,20 @@ +import { Middleware } from 'lucia' + +import { H3Event, getCookie, getRequestURL, setCookie } from 'h3' + +export function h3(): Middleware<[H3Event]> { + return ({ args, sessionCookieName }) => { + const [event] = args + return { + request: { + url: getRequestURL(event).toString(), + headers: event.headers, + method: event.method, + }, + sessionCookie: getCookie(event, sessionCookieName), + setCookie: (cookie) => { + setCookie(event, cookie.name, cookie.value, cookie.attributes) + }, + } + } +} diff --git a/server/utils/services.factory.ts b/server/utils/services.factory.ts index 2fd3645f3..dd3dd1a78 100644 --- a/server/utils/services.factory.ts +++ b/server/utils/services.factory.ts @@ -1,82 +1,69 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ // TODO: Remove once redis-mock is updated -import { - createClient, - RedisClientType, - RedisDefaultModules, - RedisFunctions, - RedisScripts, -} from 'redis' -import { promisify } from 'util' import { Config, Environment } from '~/config' -export type RedisClient = RedisClientType< - RedisDefaultModules, - RedisFunctions, - RedisScripts -> +import { createStorage, Storage } from 'unstorage' +import memoryDriver from 'unstorage/drivers/memory' +import redisDriver from 'unstorage/drivers/redis' -export async function createRedisClient(config: Config): Promise { +export function createRedisClient(config: Config): Storage { if ( config.public.environment === Environment.LocalDevelopment || config.public.environment === Environment.AzureBuild ) { - const redisMock = (await import('redis-mock')).default - const mockRedis = redisMock.createClient() - // Workaround for redis-mock being not compatible with redis@4 - // TODO: Remove this workaround once https://github.com/yeahoffline/redis-mock/issues/195 is fixed - return { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - get: promisify(mockRedis.get).bind(mockRedis), - // eslint-disable-next-line @typescript-eslint/no-misused-promises - quit: promisify(mockRedis.quit).bind(mockRedis), - /* - delete: promisify(mockRedis.del).bind(mockRedis), - flushAll: promisify(mockRedis.flushAll).bind(mockRedis), - setEx: promisify(mockRedis.setEx).bind(mockRedis), - expire: promisify(mockRedis.expire).bind(mockRedis), - */ - } as unknown as RedisClient + return createStorage({ + driver: memoryDriver(), + }) } else { - const redisConfig = { - password: config.redis.password as string | undefined, - socket: { - port: config.redis.port, - host: config.redis.host, - tls: true as true | undefined, - }, + function createRedisConfig() { + switch (config.public.environment) { + case Environment.Production: + return { + cluster: [ + { + port: config.redis.port, + host: config.redis.host, + }, + ], + clusterOptions: { + redisOptions: { + // Azure needs a TLS connection to Redis + tls: { servername: config.redis.host }, + password: config.redis.password, + }, + }, + } + case Environment.CI: + default: + // Redis on Github Actions does not need a password + return { + port: config.redis.port, + host: config.redis.host, + } + } } - // Only Azure needs a TLS connection to Redis - if (config.public.environment !== Environment.Production) { - delete redisConfig.socket.tls - } - // Redis on Github Actions does not need a password - if (config.public.environment === Environment.CI) { - delete redisConfig.password - } - const client = createClient(redisConfig) - // Log errors - // The 'error' handler is important, since otherwise errors in the redis connection bring down the whole server/process - // see https://github.com/redis/node-redis/issues/2032#issuecomment-1116883257 - client.on('error', (err) => { - console.error('Redis client:', err) + // Create redis instance to test connection + /* + const redisClient = new Redis(createRedisConfig()) + redisClient.on('error', (error) => { + console.error('Redis error', error) }) - client.on('connect', () => { - console.debug('Redis client: connected') + redisClient.on('ready', () => { + console.warn('Redis ready') }) - client.on('reconnecting', () => { - console.debug('Redis client: reconnecting') + redisClient.on('connect', () => { + console.warn('Redis connected') }) - client.on('ready', () => { - console.debug('Redis client: ready') + redisClient.on('close', () => { + console.warn('Redis closed') + }) + await redisClient.set('test', 'test') + */ + + return createStorage({ + driver: redisDriver({ + base: '{unstorage}', + ...createRedisConfig(), + }), }) - try { - await client.connect() - } catch (exception) { - console.error('Error while connection to redis') - console.error(redisConfig) - throw exception - } - return client } } diff --git a/shims-lucia.d.ts b/shims-lucia.d.ts new file mode 100644 index 000000000..f3d0acd4b --- /dev/null +++ b/shims-lucia.d.ts @@ -0,0 +1,14 @@ +import type { User } from '@prisma/client' + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace, @typescript-eslint/no-unused-vars -- https://github.com/lucia-auth/lucia/issues/1074 + namespace Lucia { + type DatabaseUserAttributes = User + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface DatabaseSessionAttributes {} + interface Auth { + getUserAttributes: (user: DatabaseUserAttributes) => User + getSessionAttributes: () => {} + } + } +} diff --git a/test/apollo.server.ts b/test/apollo.server.ts index db41cf004..63a52397a 100644 --- a/test/apollo.server.ts +++ b/test/apollo.server.ts @@ -30,16 +30,8 @@ export async function createAuthenticatedClient(): Promise { executeOperation: async (operation) => { return await server.executeOperation(operation, { contextValue: { - getUser: () => user, - isAuthenticated: () => true, - isUnauthenticated: () => false, - authenticate: (_strategyName) => { - throw new Error('Not implemented') - }, - login: () => { - throw new Error('Not implemented') - }, - logout: () => { + getUser: () => Promise.resolve(user), + setSession: () => { throw new Error('Not implemented') }, }, diff --git a/test/context.helper.ts b/test/context.helper.ts index 887337434..ee96fbb54 100644 --- a/test/context.helper.ts +++ b/test/context.helper.ts @@ -3,8 +3,6 @@ import { Context } from '~/server/context' export function createUnauthenticatedContext(): MockProxy { const context = mock() - context.getUser.mockImplementation(() => null) - context.isAuthenticated.mockImplementation(() => false) - context.isUnauthenticated.mockImplementation(() => true) + context.getUser.mockImplementation(() => Promise.resolve(null)) return context } diff --git a/test/global.setup.ts b/test/global.setup.ts index 842a93932..4f8327905 100644 --- a/test/global.setup.ts +++ b/test/global.setup.ts @@ -20,13 +20,16 @@ globalThis.Reflect = Reflect registerClasses() // Setup services for integration tests -beforeAll(async (context) => { +beforeAll((context) => { const isIntegrationTest = context.filepath?.endsWith('integration.test.ts') ?? false if (isIntegrationTest) { const config = constructConfig() - const redisClient = await createRedisClient(config) + register('Config', { + useValue: config, + }) + const redisClient = createRedisClient(config) register('RedisClient', { useValue: redisClient, }) @@ -37,9 +40,7 @@ beforeAll(async (context) => { }) return async () => { - if ('disconnect' in redisClient) { - await redisClient.disconnect() - } + await redisClient.dispose() await prismaClient.$disconnect() } } diff --git a/yarn.lock b/yarn.lock index 15d58a665..f47cfc4c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4407,6 +4407,28 @@ __metadata: languageName: node linkType: hard +"@lucia-auth/adapter-prisma@npm:^3.0.1": + version: 3.0.1 + resolution: "@lucia-auth/adapter-prisma@npm:3.0.1" + peerDependencies: + "@prisma/client": ^4.2.0 || ^5.0.0 + lucia: ^2.0.0 + checksum: 8e04283d63299a1af7c25403089128a3aac9926b2781dafc19e2beacb3430fc1ed14811ac8f12a084ecc6cb4b3ab2271b375f3eb44136b0e76127d7cfbe78e53 + languageName: node + linkType: hard + +"@lucia-auth/adapter-session-unstorage@npm:^2.1.0": + version: 2.1.0 + resolution: "@lucia-auth/adapter-session-unstorage@npm:2.1.0" + dependencies: + unstorage: ^1.9.0 + peerDependencies: + lucia: ^2.0.0 + unstorage: ^1.9.0 + checksum: c92d8210a525bf90a6e3f8cd8cd3c7edbdf07f0abf681909ec30a42af36d1cf8264e33c7843cb7cd1bd82b12bb283e1cd9c447c0cbdd178114cfaf7b41665d0c + languageName: node + linkType: hard + "@mapbox/node-pre-gyp@npm:^1.0.5": version: 1.0.10 resolution: "@mapbox/node-pre-gyp@npm:1.0.10" @@ -5333,62 +5355,6 @@ __metadata: languageName: node linkType: hard -"@redis/bloom@npm:1.2.0": - version: 1.2.0 - resolution: "@redis/bloom@npm:1.2.0" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: 8c214227287d6b278109098bca00afc601cf84f7da9c6c24f4fa7d3854b946170e5893aa86ed607ba017a4198231d570541c79931b98b6d50b262971022d1d6c - languageName: node - linkType: hard - -"@redis/client@npm:1.5.9": - version: 1.5.9 - resolution: "@redis/client@npm:1.5.9" - dependencies: - cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - checksum: 63ff34faca3ac076c76234901a47897a39bc971ab988508087d1513903b533620c54ee09a28f11b3d7b07464e239b6e756d76cc50694ed5b59418c514d2bf923 - languageName: node - linkType: hard - -"@redis/graph@npm:1.1.0": - version: 1.1.0 - resolution: "@redis/graph@npm:1.1.0" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: d3df807108a42929ed65269c691fe6ab7eda55de91318f02a22b2d637c1bfef8817fccd17025904f5a0be8cf1cea5941334ec9f10719336da5d8f1c54cd4997e - languageName: node - linkType: hard - -"@redis/json@npm:1.0.4": - version: 1.0.4 - resolution: "@redis/json@npm:1.0.4" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: de07f9c37abed603dec352593eb69fc8a94475e7f86b4f65b9805394492d448a1e4181db74269d80eb9dba6f3ae8a41804204821db36bb801cd7c1e30ac7ec80 - languageName: node - linkType: hard - -"@redis/search@npm:1.1.3": - version: 1.1.3 - resolution: "@redis/search@npm:1.1.3" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: 5f82616d1a868ebaf6cce46a7b6f7f53f0e92a299958ccf6d3ea099687c2f309d586e0bec9f25aa5174301f8508b54ebbec8835c1642cdb67b8b4d2f81a15872 - languageName: node - linkType: hard - -"@redis/time-series@npm:1.0.5": - version: 1.0.5 - resolution: "@redis/time-series@npm:1.0.5" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: 6bbdb0b793dcbd13518aa60a09a980f953554e4c745bfacc1611baa8098f360e0378e8ee6b7faf600a67f1de83f4b68bbec6f95a0740faee6164c14be3a30752 - languageName: node - linkType: hard - "@repeaterjs/repeater@npm:3.0.4, @repeaterjs/repeater@npm:^3.0.4": version: 3.0.4 resolution: "@repeaterjs/repeater@npm:3.0.4" @@ -6287,18 +6253,6 @@ __metadata: languageName: node linkType: hard -"@types/connect-redis@npm:^0.0.21": - version: 0.0.21 - resolution: "@types/connect-redis@npm:0.0.21" - dependencies: - "@types/express": "*" - "@types/express-session": "*" - "@types/redis": ^2.8.0 - ioredis: ^5.3.0 - checksum: edd9158f37b2e6aed30946b6e166a311cb7c4bd12dcdd3b41b9a9e1176bf48a59d601be980d11c0e04aab817dd71172a57a49b0d3a95cd0f1cdbdd474c2eabb1 - languageName: node - linkType: hard - "@types/connect@npm:*": version: 3.4.35 resolution: "@types/connect@npm:3.4.35" @@ -6371,16 +6325,7 @@ __metadata: languageName: node linkType: hard -"@types/express-session@npm:*, @types/express-session@npm:^1.17.7": - version: 1.17.7 - resolution: "@types/express-session@npm:1.17.7" - dependencies: - "@types/express": "*" - checksum: 4764eafaf4e26842e891e83fa9fee30b92cc3724d91987aac3a658322f9a69fb544dae24fc3aba6e15fe9575ed862760ebecd7ff97a947afb695284967434a65 - languageName: node - linkType: hard - -"@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.7.0": +"@types/express@npm:^4.17.13, @types/express@npm:^4.7.0": version: 4.17.17 resolution: "@types/express@npm:4.17.17" dependencies: @@ -6690,25 +6635,6 @@ __metadata: languageName: node linkType: hard -"@types/passport-strategy@npm:^0.2.35": - version: 0.2.35 - resolution: "@types/passport-strategy@npm:0.2.35" - dependencies: - "@types/express": "*" - "@types/passport": "*" - checksum: e5949063ad8977ce1f7cf2a5df6bc379c4d43a959c9946bfed6570ef379c6c12d1713212a29b3b5aafc631c5ebe9bfe082116f3e75ca1bf45d79dd889671532d - languageName: node - linkType: hard - -"@types/passport@npm:*, @types/passport@npm:^1.0.12, @types/passport@npm:^1.0.9": - version: 1.0.12 - resolution: "@types/passport@npm:1.0.12" - dependencies: - "@types/express": "*" - checksum: 2bce41b248566d8c2aeafcb38f105e3521d42a572e92da9edc5eb669f1b3cb02e5911c9d124191f67d432b677562a885b6ad7f56320c712df1e87ba7619eb8a4 - languageName: node - linkType: hard - "@types/pretty-hrtime@npm:^1.0.0": version: 1.0.1 resolution: "@types/pretty-hrtime@npm:1.0.1" @@ -6737,24 +6663,6 @@ __metadata: languageName: node linkType: hard -"@types/redis-mock@npm:^0.17.1": - version: 0.17.1 - resolution: "@types/redis-mock@npm:0.17.1" - dependencies: - "@types/redis": ^2.8.0 - checksum: b3c61974f5470914b44edb4dc33cb5d4ee8d1d4f433bef8ad1ea07a78b8d90963be17008ecb451d0f005aa927c2ad1b9c31583dbb52e0db6eaa9b8b0fc96df95 - languageName: node - linkType: hard - -"@types/redis@npm:^2.8.0": - version: 2.8.32 - resolution: "@types/redis@npm:2.8.32" - dependencies: - "@types/node": "*" - checksum: 2b12103e05977941870c9a248f6ea51f4b7ad7e0f16a7403799c2ed1b3e63b60f693c39f9186be0ea02776934c4595ddcd2a5bde41e530aaad42d26449f6a669 - languageName: node - linkType: hard - "@types/resolve@npm:1.20.2": version: 1.20.2 resolution: "@types/resolve@npm:1.20.2" @@ -6871,15 +6779,6 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^7.4.7": - version: 7.4.7 - resolution: "@types/ws@npm:7.4.7" - dependencies: - "@types/node": "*" - checksum: b4c9b8ad209620c9b21e78314ce4ff07515c0cadab9af101c1651e7bfb992d7fd933bd8b9c99d110738fd6db523ed15f82f29f50b45510288da72e964dedb1a3 - languageName: node - linkType: hard - "@types/ws@npm:^8.0.0": version: 8.5.4 resolution: "@types/ws@npm:8.5.4" @@ -9539,7 +9438,7 @@ __metadata: languageName: node linkType: hard -"cluster-key-slot@npm:1.1.2, cluster-key-slot@npm:^1.1.0": +"cluster-key-slot@npm:^1.1.0": version: 1.1.2 resolution: "cluster-key-slot@npm:1.1.2" checksum: be0ad2d262502adc998597e83f9ded1b80f827f0452127c5a37b22dfca36bab8edf393f7b25bb626006fb9fb2436106939ede6d2d6ecf4229b96a47f27edd681 @@ -9820,15 +9719,6 @@ __metadata: languageName: node linkType: hard -"connect-redis@npm:^7.1.0": - version: 7.1.0 - resolution: "connect-redis@npm:7.1.0" - peerDependencies: - express-session: ">=1" - checksum: fb493fae8910a2d61e3babd579c6344c56d7d31d67669d9941465ef00eb54ef9c1a35ccfa761136d36fa38d1f82ebe48c2b10888f292d55b4aadb8ecf1fddf34 - languageName: node - linkType: hard - "consola@npm:^3.1.0, consola@npm:^3.2.3": version: 3.2.3 resolution: "consola@npm:3.2.3" @@ -9901,13 +9791,6 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.4.2": - version: 0.4.2 - resolution: "cookie@npm:0.4.2" - checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b - languageName: node - linkType: hard - "cookie@npm:0.5.0, cookie@npm:^0.5.0": version: 0.5.0 resolution: "cookie@npm:0.5.0" @@ -12065,38 +11948,6 @@ __metadata: languageName: node linkType: hard -"express-session@npm:1.17.3": - version: 1.17.3 - resolution: "express-session@npm:1.17.3" - dependencies: - cookie: 0.4.2 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: ~2.0.0 - on-headers: ~1.0.2 - parseurl: ~1.3.3 - safe-buffer: 5.2.1 - uid-safe: ~2.1.5 - checksum: 1021a793433cbc6a1b32c803fcb2daa1e03a8f50dd907e8745ae57994370315a5cfde5b6ef7b062d9a9a0754ff268844bda211c08240b3a0e01014dcf1073ec5 - languageName: node - linkType: hard - -"express-session@patch:express-session@npm%3A1.17.3#./.yarn/patches/express-session-npm-1.17.3-0819dbe06c.patch::locator=jabref-online%40workspace%3A.": - version: 1.17.3 - resolution: "express-session@patch:express-session@npm%3A1.17.3#./.yarn/patches/express-session-npm-1.17.3-0819dbe06c.patch::version=1.17.3&hash=a2b087&locator=jabref-online%40workspace%3A." - dependencies: - cookie: 0.4.2 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: ~2.0.0 - on-headers: ~1.0.2 - parseurl: ~1.3.3 - safe-buffer: 5.2.1 - uid-safe: ~2.1.5 - checksum: 3d3a565248879ece729fe50dcbd6e947eda7b29cf61a592951e57a23dd9747c59099df3622b941c3124017fafdcaadd603ea872f42b427877b128007943118b9 - languageName: node - linkType: hard - "express@npm:^4.17.1, express@npm:^4.17.3": version: 4.18.2 resolution: "express@npm:4.18.2" @@ -12729,13 +12580,6 @@ __metadata: languageName: node linkType: hard -"generic-pool@npm:3.9.0": - version: 3.9.0 - resolution: "generic-pool@npm:3.9.0" - checksum: 3d89e9b2018d2e3bbf44fec78c76b2b7d56d6a484237aa9daf6ff6eedb14b0899dadd703b5d810219baab2eb28e5128fb18b29e91e602deb2eccac14492d8ca8 - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -13198,45 +13042,6 @@ __metadata: languageName: node linkType: hard -"graphql-passport@npm:^0.6.7": - version: 0.6.7 - resolution: "graphql-passport@npm:0.6.7" - dependencies: - "@types/express": ^4.17.13 - "@types/passport": ^1.0.9 - "@types/passport-strategy": ^0.2.35 - "@types/ws": ^7.4.7 - express: ^4.17.1 - graphql: ^15.5.3 - passport-strategy: ^1.0.0 - subscriptions-transport-ws: ^0.11.0 - ws: 7.x || 8.x - peerDependencies: - express: 4.x - passport: 0.x - subscriptions-transport-ws: 0.x - ws: 7.x || 8.x - dependenciesMeta: - "@types/express": - optional: true - "@types/passport": - optional: true - "@types/passport-strategy": - optional: true - "@types/ws": - optional: true - express: - optional: true - graphql: - optional: true - subscriptions-transport-ws: - optional: true - ws: - optional: true - checksum: 2ee9049de5d11900771dd00673fd332c493dccbb521cf2b9c25c900beb78e37c657d9d5577db1ad9d6a622119f64d9166fafcab170a8c6f8b1c3e58c0de6300d - languageName: node - linkType: hard - "graphql-request@npm:^6.0.0": version: 6.0.0 resolution: "graphql-request@npm:6.0.0" @@ -13310,13 +13115,6 @@ __metadata: languageName: node linkType: hard -"graphql@npm:^15.5.3": - version: 15.8.0 - resolution: "graphql@npm:15.8.0" - checksum: 423325271db8858428641b9aca01699283d1fe5b40ef6d4ac622569ecca927019fce8196208b91dd1d8eb8114f00263fe661d241d0eb40c10e5bfd650f86ec5e - languageName: node - linkType: hard - "graphql@npm:^16.6.0, graphql@npm:^16.8.0": version: 16.8.0 resolution: "graphql@npm:16.8.0" @@ -14066,7 +13864,7 @@ __metadata: languageName: node linkType: hard -"ioredis@npm:^5.3.0, ioredis@npm:^5.3.2": +"ioredis@npm:5.3.2": version: 5.3.2 resolution: "ioredis@npm:5.3.2" dependencies: @@ -14083,6 +13881,23 @@ __metadata: languageName: node linkType: hard +"ioredis@patch:ioredis@npm%3A5.3.2#./.yarn/patches/ioredis-npm-5.3.2-58471071b1.patch::locator=jabref-online%40workspace%3A.": + version: 5.3.2 + resolution: "ioredis@patch:ioredis@npm%3A5.3.2#./.yarn/patches/ioredis-npm-5.3.2-58471071b1.patch::version=5.3.2&hash=202159&locator=jabref-online%40workspace%3A." + dependencies: + "@ioredis/commands": ^1.1.1 + cluster-key-slot: ^1.1.0 + debug: ^4.3.4 + denque: ^2.1.0 + lodash.defaults: ^4.2.0 + lodash.isarguments: ^3.1.0 + redis-errors: ^1.2.0 + redis-parser: ^3.0.0 + standard-as-callback: ^2.1.0 + checksum: ab267e11f21bbacd92364f1f3c1e0affb364efe7595fc8b74d1ecc06ccbef90623bb3e135535551b53b564e01d9565447289b8b9bb2e5f3143df82fc5f1183f3 + languageName: node + linkType: hard + "ip-regex@npm:^4.0.0": version: 4.3.0 resolution: "ip-regex@npm:4.3.0" @@ -14800,6 +14615,8 @@ __metadata: "@graphql-tools/schema": ^10.0.0 "@graphql-typed-document-node/core": ^3.2.0 "@he-tree/vue": ^2.5.1 + "@lucia-auth/adapter-prisma": ^3.0.1 + "@lucia-auth/adapter-session-unstorage": ^2.1.0 "@nozomuikuta/h3-cors": ^0.2.2 "@nuxt/content": ^2.8.2 "@nuxt/devtools": ^0.8.3 @@ -14816,12 +14633,8 @@ __metadata: "@tailwindcss/line-clamp": ^0.4.4 "@tailwindcss/typography": ^0.5.10 "@types/bcryptjs": ^2.4.3 - "@types/connect-redis": ^0.0.21 - "@types/express-session": ^1.17.7 "@types/lodash": ^4.14.198 "@types/nodemailer": ^6.4.7 - "@types/passport": ^1.0.12 - "@types/redis-mock": ^0.17.1 "@types/supertest": ^2.0.12 "@types/uuid": ^9.0.4 "@types/yaireo__tagify": ^4.17.2 @@ -14844,7 +14657,6 @@ __metadata: body-scroll-lock: ^4.0.0-beta.0 chromatic: ^6.19.9 concurrently: ^8.2.1 - connect-redis: ^7.1.0 cross-env: ^7.0.3 cross-fetch: ^4.0.0 dotenv: ^16.3.1 @@ -14855,13 +14667,12 @@ __metadata: eslint-plugin-unused-imports: ^3.0.0 eslint-plugin-vitest: ^0.3.1 eslint-plugin-vue: ^9.17.0 - express-session: ^1.17.3 graphql: ^16.8.0 graphql-codegen-typescript-validation-schema: ^0.12.0 - graphql-passport: ^0.6.7 graphql-scalars: ^1.22.2 json-bigint-patch: ^0.0.8 lodash: ^4.17.21 + lucia: ^2.6.0 mount-vue-component: ^0.10.2 naive-ui: ^2.34.4 nodemailer: ^6.8.0 @@ -14870,14 +14681,11 @@ __metadata: nuxt-icon: ^0.5.0 nuxt-seo-kit: ^1.3.13 nuxt-vitest: ^0.10.5 - passport: ^0.6.0 pinia: ^2.1.6 postinstall-postinstall: ^2.1.0 prettier: ^3.0.3 prettier-plugin-organize-imports: ^3.2.3 prisma: ^5.3.1 - redis: ^4.6.8 - redis-mock: ^0.56.3 reflect-metadata: ^0.1.13 storybook: 7.0.26 storybook-vue-addon: ^0.4.0 @@ -14889,6 +14697,7 @@ __metadata: typescript: ^5.2.2 ufo: ^1.3.0 unplugin-vue-components: ^0.25.2 + unstorage: ^1.9.0 vee-validate: ^4.11.6 vitest: ^0.34.3 vitest-github-actions-reporter: ^0.10.0 @@ -15854,6 +15663,13 @@ __metadata: languageName: node linkType: hard +"lucia@npm:^2.6.0": + version: 2.6.0 + resolution: "lucia@npm:2.6.0" + checksum: 0dae01bdb2e4f1c09ab2b4577ce9b443058a8a2cde81581d6bca5e957a6a03ed298cab18f94146c034f3aafa9e4b81e9e5d49540323dfce77740e20566f76eef + languageName: node + linkType: hard + "magic-string-ast@npm:^0.1.2": version: 0.1.2 resolution: "magic-string-ast@npm:0.1.2" @@ -17435,7 +17251,7 @@ __metadata: languageName: node linkType: hard -"nitropack@npm:^2.6.3": +"nitropack@npm:2.6.3": version: 2.6.3 resolution: "nitropack@npm:2.6.3" dependencies: @@ -17514,6 +17330,85 @@ __metadata: languageName: node linkType: hard +"nitropack@patch:nitropack@npm%3A2.6.3#./.yarn/patches/nitropack-npm-2.6.3-72f7352600.patch::locator=jabref-online%40workspace%3A.": + version: 2.6.3 + resolution: "nitropack@patch:nitropack@npm%3A2.6.3#./.yarn/patches/nitropack-npm-2.6.3-72f7352600.patch::version=2.6.3&hash=a3cf21&locator=jabref-online%40workspace%3A." + dependencies: + "@cloudflare/kv-asset-handler": ^0.3.0 + "@netlify/functions": ^2.0.2 + "@rollup/plugin-alias": ^5.0.0 + "@rollup/plugin-commonjs": ^25.0.4 + "@rollup/plugin-inject": ^5.0.3 + "@rollup/plugin-json": ^6.0.0 + "@rollup/plugin-node-resolve": ^15.2.1 + "@rollup/plugin-replace": ^5.0.2 + "@rollup/plugin-terser": ^0.4.3 + "@rollup/plugin-wasm": ^6.1.3 + "@rollup/pluginutils": ^5.0.4 + "@types/http-proxy": ^1.17.11 + "@vercel/nft": ^0.23.1 + archiver: ^6.0.1 + c12: ^1.4.2 + chalk: ^5.3.0 + chokidar: ^3.5.3 + citty: ^0.1.3 + consola: ^3.2.3 + cookie-es: ^1.0.0 + defu: ^6.1.2 + destr: ^2.0.1 + dot-prop: ^8.0.2 + esbuild: ^0.19.2 + escape-string-regexp: ^5.0.0 + etag: ^1.8.1 + fs-extra: ^11.1.1 + globby: ^13.2.2 + gzip-size: ^7.0.0 + h3: ^1.8.1 + hookable: ^5.5.3 + httpxy: ^0.1.4 + is-primitive: ^3.0.1 + jiti: ^1.20.0 + klona: ^2.0.6 + knitwork: ^1.0.0 + listhen: ^1.4.8 + magic-string: ^0.30.3 + mime: ^3.0.0 + mlly: ^1.4.2 + mri: ^1.2.0 + node-fetch-native: ^1.4.0 + ofetch: ^1.3.3 + ohash: ^1.1.3 + openapi-typescript: ^6.5.4 + pathe: ^1.1.1 + perfect-debounce: ^1.0.0 + pkg-types: ^1.0.3 + pretty-bytes: ^6.1.1 + radix3: ^1.1.0 + rollup: ^3.29.0 + rollup-plugin-visualizer: ^5.9.2 + scule: ^1.0.0 + semver: ^7.5.4 + serve-placeholder: ^2.0.1 + serve-static: ^1.15.0 + std-env: ^3.4.3 + ufo: ^1.3.0 + uncrypto: ^0.1.3 + unctx: ^2.3.1 + unenv: ^1.7.4 + unimport: ^3.3.0 + unstorage: ^1.9.0 + peerDependencies: + xml2js: ^0.6.2 + peerDependenciesMeta: + xml2js: + optional: true + bin: + nitro: dist/cli/index.mjs + nitropack: dist/cli/index.mjs + checksum: 8e4c0618a2b79375b98dadd27c2411e7cc8386fae9b9efa711f959461dc59b4c4ea0a22856540bdd9da5216c14865f3fed12f5a836167ecb9dc62c31ed2e8c33 + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -18690,24 +18585,6 @@ __metadata: languageName: node linkType: hard -"passport-strategy@npm:1.x.x, passport-strategy@npm:^1.0.0": - version: 1.0.0 - resolution: "passport-strategy@npm:1.0.0" - checksum: 5086693f2508e538dffa55a338c89fe8192fb5f4478c71f80cd5890b8573419a098f4fec88b505374f60bbe9049f6f24b9f3992678612528a3370b4dc73354a2 - languageName: node - linkType: hard - -"passport@npm:^0.6.0": - version: 0.6.0 - resolution: "passport@npm:0.6.0" - dependencies: - passport-strategy: 1.x.x - pause: 0.0.1 - utils-merge: ^1.0.1 - checksum: ef932ad671d50de34765c7a53cd1e058d8331a82a6df09265a9c6c1168911aee4a7b5215803d0101110ab7f317e096b4954ca7e18fb2c33b9929f0bd17dbe159 - languageName: node - linkType: hard - "password-prompt@npm:^1.0.4": version: 1.1.2 resolution: "password-prompt@npm:1.1.2" @@ -18838,13 +18715,6 @@ __metadata: languageName: node linkType: hard -"pause@npm:0.0.1": - version: 0.0.1 - resolution: "pause@npm:0.0.1" - checksum: e96ee581b68085e6f2ba5adbcb4d4a41fe88e5b514061e76df2fe1905f0f65f4fe5a843b538e9551122c6b9184ff4be266c2ee0ea4614702f9a3d04466d9f462 - languageName: node - linkType: hard - "peek-stream@npm:^1.1.0": version: 1.1.3 resolution: "peek-stream@npm:1.1.3" @@ -20011,13 +19881,6 @@ __metadata: languageName: node linkType: hard -"random-bytes@npm:~1.0.0": - version: 1.0.0 - resolution: "random-bytes@npm:1.0.0" - checksum: 09faa256394aa2ca9754aa57e92a27c452c3e97ffb266e98bebb517332e9df7168fea393159f88d884febce949ba8bec8ddb02f03342da6c6023ecc7b155e0ae - languageName: node - linkType: hard - "randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -20236,20 +20099,6 @@ __metadata: languageName: node linkType: hard -"redis-mock@npm:0.56.3": - version: 0.56.3 - resolution: "redis-mock@npm:0.56.3" - checksum: 8c1293a59610c89a13849191de62aec9e68cb0a116e898b8fd5235235100f987caaeb5dacf56a076263474a2f556bd7fc2e7d2c52364a940f392dacb178b9ff4 - languageName: node - linkType: hard - -"redis-mock@patch:redis-mock@npm%3A0.56.3#./.yarn/patches/redis-mock-npm-0.56.3-967bd7c6ea.patch::locator=jabref-online%40workspace%3A.": - version: 0.56.3 - resolution: "redis-mock@patch:redis-mock@npm%3A0.56.3#./.yarn/patches/redis-mock-npm-0.56.3-967bd7c6ea.patch::version=0.56.3&hash=b687db&locator=jabref-online%40workspace%3A." - checksum: 7556fce00760a44f4969d62e1b622005c76c48f08667c81bf6000975b630cc59c7220437d0e4655a6eb61dea9277621389dfe2b668b8c004908158be1f652701 - languageName: node - linkType: hard - "redis-parser@npm:^3.0.0": version: 3.0.0 resolution: "redis-parser@npm:3.0.0" @@ -20259,20 +20108,6 @@ __metadata: languageName: node linkType: hard -"redis@npm:^4.6.8": - version: 4.6.8 - resolution: "redis@npm:4.6.8" - dependencies: - "@redis/bloom": 1.2.0 - "@redis/client": 1.5.9 - "@redis/graph": 1.1.0 - "@redis/json": 1.0.4 - "@redis/search": 1.1.3 - "@redis/time-series": 1.0.5 - checksum: 1626d0d739e5f2047824b77b472967af5938fba488a039bd0a5ad90814eb81cb29b7939e16adf0bc39b133165ec1bf147e4f944d0c54a2f9c32c8f004b015940 - languageName: node - linkType: hard - "reflect-metadata@npm:^0.1.13": version: 0.1.13 resolution: "reflect-metadata@npm:0.1.13" @@ -22830,15 +22665,6 @@ __metadata: languageName: node linkType: hard -"uid-safe@npm:~2.1.5": - version: 2.1.5 - resolution: "uid-safe@npm:2.1.5" - dependencies: - random-bytes: ~1.0.0 - checksum: 07536043da9a026f4a2bc397543d0ace7587449afa1d9d2c4fd3ce76af8a5263a678788bcc429dff499ef29d45843cd5ee9d05434450fcfc19cc661229f703d1 - languageName: node - linkType: hard - "ultrahtml@npm:^1.2.0, ultrahtml@npm:^1.5.2": version: 1.5.2 resolution: "ultrahtml@npm:1.5.2" @@ -23544,7 +23370,7 @@ __metadata: languageName: node linkType: hard -"utils-merge@npm:1.0.1, utils-merge@npm:^1.0.1": +"utils-merge@npm:1.0.1": version: 1.0.1 resolution: "utils-merge@npm:1.0.1" checksum: c81095493225ecfc28add49c106ca4f09cdf56bc66731aa8dabc2edbbccb1e1bfe2de6a115e5c6a380d3ea166d1636410b62ef216bb07b3feb1cfde1d95d5080 @@ -24538,7 +24364,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:7.x || 8.x, ws@npm:8.13.0, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.2.3, ws@npm:^8.4.0": +"ws@npm:8.13.0, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.2.3, ws@npm:^8.4.0": version: 8.13.0 resolution: "ws@npm:8.13.0" peerDependencies: @@ -24643,13 +24469,6 @@ __metadata: languageName: node linkType: hard -"yallist@npm:4.0.0, yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 - languageName: node - linkType: hard - "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" @@ -24657,6 +24476,13 @@ __metadata: languageName: node linkType: hard +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 + languageName: node + linkType: hard + "yaml-ast-parser@npm:^0.0.43": version: 0.0.43 resolution: "yaml-ast-parser@npm:0.0.43"