From 4fd72fd1bc87e4e0457d1a672785d5d561dc6b44 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 14 May 2024 08:35:31 +0200 Subject: [PATCH 1/7] Add services needed for sessions and end-user authentication --- app/auth/auth.server.ts | 35 +++ app/auth/passwords.server.ts | 6 + app/auth/session.server.ts | 20 ++ app/container/container.server.ts | 7 + app/users/UsersService.server.ts | 22 ++ app/utils/env.server.ts | 6 + package-lock.json | 418 ++++++++++++++++++++---------- package.json | 4 + 8 files changed, 386 insertions(+), 132 deletions(-) create mode 100644 app/auth/auth.server.ts create mode 100644 app/auth/passwords.server.ts create mode 100644 app/auth/session.server.ts create mode 100644 app/users/UsersService.server.ts diff --git a/app/auth/auth.server.ts b/app/auth/auth.server.ts new file mode 100644 index 0000000..3e48b94 --- /dev/null +++ b/app/auth/auth.server.ts @@ -0,0 +1,35 @@ +import type { Strategy } from 'remix-auth'; +import { Authenticator } from 'remix-auth'; +import { FormStrategy } from 'remix-auth-form'; +import type { UsersService } from '../users/UsersService.server'; +import type { SessionStorage } from './session.server'; + +export const CREDENTIALS_STRATEGY = 'credentials'; + +function getAuthStrategies(usersService: UsersService): Map> { + const strategies = new Map>(); + + // Add strategy to login via credentials form + strategies.set(CREDENTIALS_STRATEGY, new FormStrategy(async ({ form }) => { + const username = form.get('username'); + const password = form.get('password'); + if (typeof username !== 'string' || typeof password !== 'string') { + // TODO Check if this is the right way to handle this error + throw new Error('Username or password missing'); + } + + return usersService.getUserByCredentials(username, password); + })); + + // TODO Add other strategies, like oAuth for SSO + + return strategies; +} + +export function createAuthenticator(usersService: UsersService, sessionStorage: SessionStorage): Authenticator { + const authenticator = new Authenticator(sessionStorage); + const strategies = getAuthStrategies(usersService); + strategies.forEach((strategy, name) => authenticator.use(strategy, name)); + + return authenticator; +} diff --git a/app/auth/passwords.server.ts b/app/auth/passwords.server.ts new file mode 100644 index 0000000..c192fe8 --- /dev/null +++ b/app/auth/passwords.server.ts @@ -0,0 +1,6 @@ +import bcrypt from 'bcrypt'; + +export const hashPassword = async (plainTextPassword: string) => bcrypt.hash(plainTextPassword, 10); + +export const verifyPassword = async (plainTextPassword: string, hashedPassword: string) => + bcrypt.compare(plainTextPassword, hashedPassword); diff --git a/app/auth/session.server.ts b/app/auth/session.server.ts new file mode 100644 index 0000000..6570113 --- /dev/null +++ b/app/auth/session.server.ts @@ -0,0 +1,20 @@ +import { createCookieSessionStorage } from '@remix-run/node'; +import { env, isProd } from '../utils/env.server'; + +export type SessionData = { + userId: string; +}; + +export const createSessionStorage = () => createCookieSessionStorage({ + cookie: { + name: '__shlink_dashboard_session', + httpOnly: true, + maxAge: 30 * 60, // 30 minutes + path: '/', + sameSite: 'lax', + secrets: env.SHLINK_DASHBOARD_SESSION_SECRETS ?? ['s3cr3t1'], + secure: isProd(), + }, +}); + +export type SessionStorage = ReturnType; diff --git a/app/container/container.server.ts b/app/container/container.server.ts index 5131fb7..de95bee 100644 --- a/app/container/container.server.ts +++ b/app/container/container.server.ts @@ -1,9 +1,13 @@ import Bottle from 'bottlejs'; +import { Authenticator } from 'remix-auth'; import { apiClientBuilder } from '../api/apiClientBuilder.server'; +import { createAuthenticator } from '../auth/auth.server'; +import { createSessionStorage } from '../auth/session.server'; import { appDataSource } from '../db/data-source.server'; import { ServersService } from '../servers/ServersService.server'; import { SettingsService } from '../settings/SettingsService.server'; import { TagsService } from '../tags/TagsService.server'; +import { UsersService } from '../users/UsersService.server'; const bottle = new Bottle(); @@ -12,7 +16,10 @@ bottle.serviceFactory('em', () => appDataSource.manager); bottle.service(TagsService.name, TagsService, 'em'); bottle.service(ServersService.name, ServersService, 'em'); bottle.service(SettingsService.name, SettingsService, 'em'); +bottle.service(UsersService.name, UsersService, 'em'); bottle.constant('apiClientBuilder', apiClientBuilder); +bottle.serviceFactory('sessionStorage', createSessionStorage); +bottle.serviceFactory(Authenticator.name, createAuthenticator, 'em', 'sessionStorage'); export const { container: serverContainer } = bottle; diff --git a/app/users/UsersService.server.ts b/app/users/UsersService.server.ts new file mode 100644 index 0000000..9457b5b --- /dev/null +++ b/app/users/UsersService.server.ts @@ -0,0 +1,22 @@ +import type { EntityManager } from 'typeorm'; +import { verifyPassword } from '../auth/passwords.server'; +import type { User } from '../entities/User'; +import { UserEntity } from '../entities/User'; + +export class UsersService { + constructor(private readonly em: EntityManager) {} + + async getUserByCredentials(username: string, password: string): Promise { + const user = await this.em.findOneBy(UserEntity, { username }); + if (!user) { + throw new Error(`User not found with username ${username}`); + } + + const isPasswordCorrect = await verifyPassword(password, user.password); + if (!isPasswordCorrect) { + throw new Error(`Incorrect password for user ${username}`); + } + + return user; + } +} diff --git a/app/utils/env.server.ts b/app/utils/env.server.ts index aef5228..6ed4907 100644 --- a/app/utils/env.server.ts +++ b/app/utils/env.server.ts @@ -14,6 +14,12 @@ const envVariables = z.object({ SHLINK_DASHBOARD_DB_USER: z.string().optional(), SHLINK_DASHBOARD_DB_PASSWORD: z.string().optional(), SHLINK_DASHBOARD_DB_NAME: z.string().optional(), + + // Sessions + SHLINK_DASHBOARD_SESSION_SECRETS: z.string().transform( + // Split the comma-separated list of secrets + (secrets) => secrets.split(',').map((v) => v.trim()), + ).optional(), }); export const env = envVariables.parse(process.env); diff --git a/package-lock.json b/package-lock.json index 583aaf2..b84c4ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@shlinkio/shlink-frontend-kit": "^0.5.1", "@shlinkio/shlink-js-sdk": "^1.1.0", "@shlinkio/shlink-web-component": "^0.6.2", + "bcrypt": "^5.1.1", "bootstrap": "5.2.3", "bottlejs": "^2.0.1", "express": "^4.19.2", @@ -21,6 +22,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "reactstrap": "^9.2.2", + "remix-auth": "^3.6.0", + "remix-auth-form": "^1.5.0", "typeorm": "^0.3.20", "zod": "^3.23.8" }, @@ -32,6 +35,7 @@ "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.2", "@total-typescript/shoehorn": "^0.1.2", + "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.21", "@types/node": "^20.12.11", "@types/react": "^18.3.1", @@ -1528,6 +1532,170 @@ "integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==", "dev": true }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@mdx-js/mdx": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz", @@ -1817,32 +1985,6 @@ } } }, - "node_modules/@remix-run/dev/node_modules/@remix-run/server-runtime": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.9.2.tgz", - "integrity": "sha512-dX37FEeMVVg7KUbpRhX4hD0nUY0Sscz/qAjU4lYCdd6IzwJGariTmz+bQTXKCjploZuXj09OQZHSOS/ydkUVDA==", - "dev": true, - "dependencies": { - "@remix-run/router": "1.16.1", - "@types/cookie": "^0.6.0", - "@web3-storage/multipart-parser": "^1.0.0", - "cookie": "^0.6.0", - "set-cookie-parser": "^2.4.8", - "source-map": "^0.7.3", - "turbo-stream": "^2.0.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@remix-run/express": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@remix-run/express/-/express-2.9.2.tgz", @@ -1888,31 +2030,6 @@ } } }, - "node_modules/@remix-run/node/node_modules/@remix-run/server-runtime": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.9.2.tgz", - "integrity": "sha512-dX37FEeMVVg7KUbpRhX4hD0nUY0Sscz/qAjU4lYCdd6IzwJGariTmz+bQTXKCjploZuXj09OQZHSOS/ydkUVDA==", - "dependencies": { - "@remix-run/router": "1.16.1", - "@types/cookie": "^0.6.0", - "@web3-storage/multipart-parser": "^1.0.0", - "cookie": "^0.6.0", - "set-cookie-parser": "^2.4.8", - "source-map": "^0.7.3", - "turbo-stream": "^2.0.0" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@remix-run/react": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@remix-run/react/-/react-2.9.2.tgz", @@ -1938,7 +2055,15 @@ } } }, - "node_modules/@remix-run/react/node_modules/@remix-run/server-runtime": { + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@remix-run/server-runtime": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.9.2.tgz", "integrity": "sha512-dX37FEeMVVg7KUbpRhX4hD0nUY0Sscz/qAjU4lYCdd6IzwJGariTmz+bQTXKCjploZuXj09OQZHSOS/ydkUVDA==", @@ -1963,14 +2088,6 @@ } } }, - "node_modules/@remix-run/router": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", - "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@remix-run/testing": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@remix-run/testing/-/testing-2.9.2.tgz", @@ -2551,6 +2668,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -3403,9 +3529,7 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "optional": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/abort-controller": { "version": "3.0.0", @@ -3591,9 +3715,7 @@ "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "dev": true, - "optional": true + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, "node_modules/are-we-there-yet": { "version": "3.0.1", @@ -3910,6 +4032,24 @@ } ] }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bcrypt/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4311,7 +4451,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "devOptional": true, "engines": { "node": ">=10" } @@ -4565,8 +4704,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "optional": true, "bin": { "color-support": "bin.js" } @@ -4601,8 +4738,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/confusing-browser-globals": { "version": "1.0.11", @@ -4613,9 +4749,7 @@ "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, - "optional": true + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, "node_modules/content-disposition": { "version": "0.5.4", @@ -5109,9 +5243,7 @@ "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, - "optional": true + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, "node_modules/depd": { "version": "2.0.0", @@ -5143,7 +5275,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "devOptional": true, "engines": { "node": ">=8" } @@ -5285,7 +5416,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -5295,7 +5425,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6761,8 +6890,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -7179,9 +7307,7 @@ "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, - "optional": true + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, "node_modules/hasown": { "version": "2.0.2", @@ -7503,7 +7629,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -10071,7 +10196,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "devOptional": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -10084,7 +10208,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -10095,14 +10218,12 @@ "node_modules/minizlib/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "devOptional": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "devOptional": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -10225,6 +10346,44 @@ "node": "^16 || ^18 || >= 20" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -10321,8 +10480,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dev": true, - "optional": true, "dependencies": { "abbrev": "1" }, @@ -10584,7 +10741,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, "dependencies": { "wrappy": "1" } @@ -10802,7 +10958,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11749,7 +11904,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "devOptional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -11966,6 +12120,35 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remix-auth": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.6.0.tgz", + "integrity": "sha512-mxlzLYi+/GKQSaXIqIw15dxAT1wm+93REAeDIft2unrKDYnjaGhhpapyPhdbALln86wt9lNAk21znfRss3fG7Q==", + "dependencies": { + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@remix-run/react": "^1.0.0 || ^2.0.0", + "@remix-run/server-runtime": "^1.0.0 || ^2.0.0" + } + }, + "node_modules/remix-auth-form": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/remix-auth-form/-/remix-auth-form-1.5.0.tgz", + "integrity": "sha512-xWM7T41vi4ZsIxL3f8gz/D6g2mxrnYF7LnG+rG3VqwHh6l13xCoKLraxzWRdbKMVKKQCMISKZRXAeJh9/PQwBA==", + "peerDependencies": { + "@remix-run/server-runtime": "^1.0.0 || ^2.0.0", + "remix-auth": "^3.6.0" + } + }, + "node_modules/remix-auth/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -12076,7 +12259,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -12091,7 +12273,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -12101,7 +12282,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -12121,7 +12301,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12299,7 +12478,6 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "devOptional": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -12314,7 +12492,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -12325,8 +12502,7 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "devOptional": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { "version": "0.18.0", @@ -12373,9 +12549,7 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "optional": true + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/set-cookie-parser": { "version": "2.6.0", @@ -12476,8 +12650,7 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-concat": { "version": "1.0.1", @@ -12774,7 +12947,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "devOptional": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -12783,7 +12955,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "devOptional": true, "funding": [ { "type": "github", @@ -13089,7 +13260,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "devOptional": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -13150,7 +13320,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "devOptional": true, "dependencies": { "minipass": "^3.0.0" }, @@ -13162,7 +13331,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -13174,7 +13342,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "devOptional": true, "engines": { "node": ">=8" } @@ -13182,8 +13349,7 @@ "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "devOptional": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/test-exclude": { "version": "6.0.0", @@ -14461,8 +14627,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "devOptional": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utils-merge": { "version": "1.0.1", @@ -15521,8 +15686,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "optional": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -15531,8 +15694,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "optional": true, "engines": { "node": ">=8" } @@ -15540,16 +15701,12 @@ "node_modules/wide-align/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "optional": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/wide-align/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "optional": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -15563,8 +15720,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "optional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -15656,8 +15811,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { "version": "7.5.9", diff --git a/package.json b/package.json index 70e6603..641ef79 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@shlinkio/shlink-frontend-kit": "^0.5.1", "@shlinkio/shlink-js-sdk": "^1.1.0", "@shlinkio/shlink-web-component": "^0.6.2", + "bcrypt": "^5.1.1", "bootstrap": "5.2.3", "bottlejs": "^2.0.1", "express": "^4.19.2", @@ -36,6 +37,8 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "reactstrap": "^9.2.2", + "remix-auth": "^3.6.0", + "remix-auth-form": "^1.5.0", "typeorm": "^0.3.20", "zod": "^3.23.8" }, @@ -47,6 +50,7 @@ "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.5.2", "@total-typescript/shoehorn": "^0.1.2", + "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.21", "@types/node": "^20.12.11", "@types/react": "^18.3.1", From dcd2453b12565b042a5cad508928959d2ce94ae7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 14 May 2024 08:43:39 +0200 Subject: [PATCH 2/7] Create passwords test --- test/auth/passwords.server.test.ts | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/auth/passwords.server.test.ts diff --git a/test/auth/passwords.server.test.ts b/test/auth/passwords.server.test.ts new file mode 100644 index 0000000..6bada01 --- /dev/null +++ b/test/auth/passwords.server.test.ts @@ -0,0 +1,33 @@ +import { hashPassword, verifyPassword } from '../../app/auth/passwords.server'; + +describe('passwords', () => { + it('can hash and then verify a password', async () => { + const password1 = 'the_password'; + const password2 = 'P4SSW0RD'; + + const [password1hash1, password1hash2, password2hash1, password2hash2] = await Promise.all([ + hashPassword(password1), + hashPassword(password1), + hashPassword(password2), + hashPassword(password2), + ]); + + const [validChecks, invalidChecks] = await Promise.all([ + Promise.all([ + verifyPassword(password1, password1hash1), + verifyPassword(password1, password1hash2), + verifyPassword(password2, password2hash1), + verifyPassword(password2, password2hash2), + ]), + Promise.all([ + verifyPassword(password2, password1hash1), + verifyPassword(password2, password1hash2), + verifyPassword(password1, password2hash1), + verifyPassword(password1, password2hash2), + ]), + ]); + + expect(validChecks.every((isValid) => isValid)).toEqual(true); + expect(invalidChecks.every((isValid) => !isValid)).toEqual(true); + }); +}); From 148dd386639b7767f9954a0254cb66010b886eae Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 14 May 2024 08:53:39 +0200 Subject: [PATCH 3/7] Create UsersService test --- .../server.$serverId.tags.colors.test.ts | 1 - test/servers/ServersService.server.test.ts | 1 - test/setup.ts | 5 +++ test/tags/TagsService.server.test.ts | 1 - test/users/UsersService.server.test.ts | 41 +++++++++++++++++++ 5 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 test/users/UsersService.server.test.ts diff --git a/test/routes/server.$serverId.tags.colors.test.ts b/test/routes/server.$serverId.tags.colors.test.ts index 31942df..d053293 100644 --- a/test/routes/server.$serverId.tags.colors.test.ts +++ b/test/routes/server.$serverId.tags.colors.test.ts @@ -9,7 +9,6 @@ describe('server.$serverId.tags.colors', () => { beforeEach(() => { tagsService = fromPartial({ updateTagColors }); - vi.clearAllMocks(); }); it('updates tags in action and returns response', async () => { diff --git a/test/servers/ServersService.server.test.ts b/test/servers/ServersService.server.test.ts index 25ea47d..26b2a38 100644 --- a/test/servers/ServersService.server.test.ts +++ b/test/servers/ServersService.server.test.ts @@ -11,7 +11,6 @@ describe('ServersService', () => { beforeEach(() => { em = fromPartial({ findOneBy }); service = new ServersService(em); - vi.clearAllMocks(); }); describe('getByPublicId', () => { diff --git a/test/setup.ts b/test/setup.ts index a812a5d..d839ada 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -14,6 +14,11 @@ axe.configure({ ], }); +// Clears all mocks after every test +afterEach(() => { + vi.clearAllMocks(); +}); + if (typeof HTMLCanvasElement !== 'undefined') { HTMLCanvasElement.prototype.getContext = (() => {}) as any; } diff --git a/test/tags/TagsService.server.test.ts b/test/tags/TagsService.server.test.ts index 5268dfe..39089d3 100644 --- a/test/tags/TagsService.server.test.ts +++ b/test/tags/TagsService.server.test.ts @@ -31,7 +31,6 @@ describe('TagsService', () => { beforeEach(() => { tagsService = new TagsService(em); transaction.mockImplementation((callback) => callback(em)); - vi.clearAllMocks(); }); describe('tagColors', () => { diff --git a/test/users/UsersService.server.test.ts b/test/users/UsersService.server.test.ts new file mode 100644 index 0000000..02559d9 --- /dev/null +++ b/test/users/UsersService.server.test.ts @@ -0,0 +1,41 @@ +import { fromPartial } from '@total-typescript/shoehorn'; +import type { EntityManager } from 'typeorm'; +import { hashPassword } from '../../app/auth/passwords.server'; +import type { User } from '../../app/entities/User'; +import { UsersService } from '../../app/users/UsersService.server'; + +describe('UsersService', () => { + const findOneBy = vi.fn(); + let em: EntityManager; + let usersService: UsersService; + + beforeEach(() => { + em = fromPartial({ findOneBy }); + usersService = new UsersService(em); + }); + + describe('getUserByCredentials', () => { + it('throws when user is not found', async () => { + findOneBy.mockResolvedValue(null); + await expect(() => usersService.getUserByCredentials('foo', 'bar')).rejects.toEqual( + new Error('User not found with username foo'), + ); + }); + + it('throws if password does not match', async () => { + findOneBy.mockResolvedValue(fromPartial({ password: await hashPassword('the right one') })); + await expect(() => usersService.getUserByCredentials('foo', 'bar')).rejects.toEqual( + new Error('Incorrect password for user foo'), + ); + }); + + it('returns user if password is correct', async () => { + const expectedUser = fromPartial({ password: await hashPassword('bar') }); + findOneBy.mockResolvedValue(expectedUser); + + const result = await usersService.getUserByCredentials('foo', 'bar'); + + expect(result).toEqual(expectedUser); + }); + }); +}); From f3dfecdd0c75e1a40acb63b3cf1627700fd104c8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 15 May 2024 09:14:46 +0200 Subject: [PATCH 4/7] Add user authentication and session management --- .eslintrc | 5 + app/auth/auth.server.ts | 8 +- app/auth/passwords.server.ts | 6 +- app/auth/session.server.ts | 5 +- app/container/container.server.ts | 2 +- app/db/data-source.server.ts | 2 +- app/root.tsx | 16 +- app/routes/login.tsx | 62 ++++++ app/routes/server.$serverId.$.tsx | 2 +- package-lock.json | 342 +++++++++--------------------- package.json | 2 +- server.ts | 7 +- 12 files changed, 202 insertions(+), 257 deletions(-) create mode 100644 app/routes/login.tsx diff --git a/.eslintrc b/.eslintrc index 396db1f..dff3441 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,6 +6,11 @@ "parserOptions": { "project": "./tsconfig.json" }, + "rules": { + "jsx-a11y/label-has-associated-control": ["error", { + "controlComponents": ["Input", "DateInput"] + }] + }, "overrides": [ { "files": ["**/app/routes/**", "vite*.config.ts"], diff --git a/app/auth/auth.server.ts b/app/auth/auth.server.ts index 3e48b94..11914c9 100644 --- a/app/auth/auth.server.ts +++ b/app/auth/auth.server.ts @@ -2,7 +2,7 @@ import type { Strategy } from 'remix-auth'; import { Authenticator } from 'remix-auth'; import { FormStrategy } from 'remix-auth-form'; import type { UsersService } from '../users/UsersService.server'; -import type { SessionStorage } from './session.server'; +import type { SessionData, SessionStorage } from './session.server'; export const CREDENTIALS_STRATEGY = 'credentials'; @@ -10,15 +10,15 @@ function getAuthStrategies(usersService: UsersService): Map>(); // Add strategy to login via credentials form - strategies.set(CREDENTIALS_STRATEGY, new FormStrategy(async ({ form }) => { + strategies.set(CREDENTIALS_STRATEGY, new FormStrategy(async ({ form }): Promise => { const username = form.get('username'); const password = form.get('password'); if (typeof username !== 'string' || typeof password !== 'string') { - // TODO Check if this is the right way to handle this error throw new Error('Username or password missing'); } - return usersService.getUserByCredentials(username, password); + const user = await usersService.getUserByCredentials(username, password); + return { userId: user.id }; })); // TODO Add other strategies, like oAuth for SSO diff --git a/app/auth/passwords.server.ts b/app/auth/passwords.server.ts index c192fe8..90b8cb4 100644 --- a/app/auth/passwords.server.ts +++ b/app/auth/passwords.server.ts @@ -1,6 +1,6 @@ -import bcrypt from 'bcrypt'; +import * as argon2 from 'argon2'; -export const hashPassword = async (plainTextPassword: string) => bcrypt.hash(plainTextPassword, 10); +export const hashPassword = async (plainTextPassword: string) => argon2.hash(plainTextPassword); export const verifyPassword = async (plainTextPassword: string, hashedPassword: string) => - bcrypt.compare(plainTextPassword, hashedPassword); + argon2.verify(hashedPassword, plainTextPassword); diff --git a/app/auth/session.server.ts b/app/auth/session.server.ts index 6570113..92753b5 100644 --- a/app/auth/session.server.ts +++ b/app/auth/session.server.ts @@ -2,12 +2,13 @@ import { createCookieSessionStorage } from '@remix-run/node'; import { env, isProd } from '../utils/env.server'; export type SessionData = { - userId: string; + userId: number; + [key: string]: unknown; }; export const createSessionStorage = () => createCookieSessionStorage({ cookie: { - name: '__shlink_dashboard_session', + name: 'shlink_dashboard_session', httpOnly: true, maxAge: 30 * 60, // 30 minutes path: '/', diff --git a/app/container/container.server.ts b/app/container/container.server.ts index de95bee..ef196f4 100644 --- a/app/container/container.server.ts +++ b/app/container/container.server.ts @@ -20,6 +20,6 @@ bottle.service(UsersService.name, UsersService, 'em'); bottle.constant('apiClientBuilder', apiClientBuilder); bottle.serviceFactory('sessionStorage', createSessionStorage); -bottle.serviceFactory(Authenticator.name, createAuthenticator, 'em', 'sessionStorage'); +bottle.serviceFactory(Authenticator.name, createAuthenticator, UsersService.name, 'sessionStorage'); export const { container: serverContainer } = bottle; diff --git a/app/db/data-source.server.ts b/app/db/data-source.server.ts index 16b660c..9cd1e5d 100644 --- a/app/db/data-source.server.ts +++ b/app/db/data-source.server.ts @@ -32,7 +32,7 @@ function resolveOptions(): DataSourceOptions { password: env.SHLINK_DASHBOARD_DB_PASSWORD, database: env.SHLINK_DASHBOARD_DB_NAME ?? 'shlink_dashboard', synchronize: false, - logging: !isProd(), + logging: false, entities: [UserEntity, SettingsEntity, TagEntity, ServerEntity], migrations: ['app/db/migrations/*.ts'], // FIXME These won't work when bundling for prod. Revisit }; diff --git a/app/root.tsx b/app/root.tsx index bb2506e..4da667f 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,15 +1,29 @@ +import type { LoaderFunctionArgs } from '@remix-run/node'; import { Links, Meta, Outlet, Scripts } from '@remix-run/react'; +import { Authenticator } from 'remix-auth'; import { MainHeader } from './common/MainHeader'; +import { serverContainer } from './container/container.server'; import { appDataSource } from './db/data-source.server'; import './index.scss'; -export async function loader() { +export async function loader( + { request }: LoaderFunctionArgs, + authenticator: Authenticator = serverContainer[Authenticator.name], +) { if (!appDataSource.isInitialized) { console.log('Initializing database connection...'); await appDataSource.initialize(); console.log('Database connection initialized'); } + const { pathname } = new URL(request.url); + const isPublicRoute = ['/login'].includes(pathname); + if (!isPublicRoute) { + await authenticator.isAuthenticated(request, { + failureRedirect: `/login?redirect-to=${encodeURIComponent(pathname)}`, + }); + } + return {}; } diff --git a/app/routes/login.tsx b/app/routes/login.tsx new file mode 100644 index 0000000..01b7d99 --- /dev/null +++ b/app/routes/login.tsx @@ -0,0 +1,62 @@ +import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node'; +import { json } from '@remix-run/node'; +import { useLoaderData } from '@remix-run/react'; +import { useId } from 'react'; +import { Button, Input } from 'reactstrap'; +import { Authenticator } from 'remix-auth'; +import { CREDENTIALS_STRATEGY } from '../auth/auth.server'; +import type { SessionStorage } from '../auth/session.server'; +import { serverContainer } from '../container/container.server'; + +export async function action( + { request }: ActionFunctionArgs, + authenticator: Authenticator = serverContainer[Authenticator.name], +) { + const { searchParams } = new URL(request.url); + return authenticator.authenticate(CREDENTIALS_STRATEGY, request, { + successRedirect: searchParams.get('redirect-to') ?? '/', // TODO Make sure "redirect-to" is a relative URL + failureRedirect: request.url, + }); +} + +export async function loader( + { request }: LoaderFunctionArgs, + authenticator: Authenticator = serverContainer[Authenticator.name], + { getSession, commitSession }: SessionStorage = serverContainer.sessionStorage, +) { + // If the user is already authenticated redirect to home + await authenticator.isAuthenticated(request, { successRedirect: '/' }); + + const session = await getSession(request.headers.get('cookie')); + const error = session.get(authenticator.sessionErrorKey); + return json({ hasError: !!error }, { + headers: { + 'Set-Cookie': await commitSession(session), + }, + }); +} + +export default function Login() { + const usernameId = useId(); + const passwordId = useId(); + const { hasError } = useLoaderData(); + + return ( +
+
+ + +
+
+ + +
+ + {hasError &&
Username or password are incorrect
} +
+ ); +} diff --git a/app/routes/server.$serverId.$.tsx b/app/routes/server.$serverId.$.tsx index 3732b3f..9c9338c 100644 --- a/app/routes/server.$serverId.$.tsx +++ b/app/routes/server.$serverId.$.tsx @@ -27,7 +27,7 @@ export async function loader( export default function ShlinkWebComponentContainer() { const [component, setComponent] = useState(null); - const { settings, tagColors } = useLoaderData>(); + const { settings, tagColors } = useLoaderData(); const params = useParams(); const { serverId } = params; const { pathname } = useLocation(); diff --git a/package-lock.json b/package-lock.json index b84c4ac..9d763c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@shlinkio/shlink-frontend-kit": "^0.5.1", "@shlinkio/shlink-js-sdk": "^1.1.0", "@shlinkio/shlink-web-component": "^0.6.2", - "bcrypt": "^5.1.1", + "argon2": "^0.40.1", "bootstrap": "5.2.3", "bottlejs": "^2.0.1", "express": "^4.19.2", @@ -1532,170 +1532,6 @@ "integrity": "sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==", "dev": true }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@mdx-js/mdx": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz", @@ -1845,6 +1681,14 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3529,7 +3373,9 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "optional": true }, "node_modules/abort-controller": { "version": "3.0.0", @@ -3715,7 +3561,9 @@ "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true, + "optional": true }, "node_modules/are-we-there-yet": { "version": "3.0.1", @@ -3737,6 +3585,20 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "dev": true }, + "node_modules/argon2": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.40.1.tgz", + "integrity": "sha512-DjtHDwd7pm12qeWyfihHoM8Bn5vGcgH6sKwgPqwNYroRmxlrzadHEvMyuvQxN/V8YSyRRKD5x6ito09q1e9OyA==", + "hasInstallScript": true, + "dependencies": { + "@phc/format": "^1.0.0", + "node-addon-api": "^7.1.0", + "node-gyp-build": "^4.8.0" + }, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4032,24 +3894,6 @@ } ] }, - "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", - "hasInstallScript": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/bcrypt/node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4451,6 +4295,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "devOptional": true, "engines": { "node": ">=10" } @@ -4704,6 +4549,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "optional": true, "bin": { "color-support": "bin.js" } @@ -4738,7 +4585,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/confusing-browser-globals": { "version": "1.0.11", @@ -4749,7 +4597,9 @@ "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "optional": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -5243,7 +5093,9 @@ "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "optional": true }, "node_modules/depd": { "version": "2.0.0", @@ -5275,6 +5127,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "devOptional": true, "engines": { "node": ">=8" } @@ -5416,6 +5269,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -5425,6 +5279,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -6890,7 +6745,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -7307,7 +7163,9 @@ "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "optional": true }, "node_modules/hasown": { "version": "2.0.2", @@ -7629,6 +7487,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -10196,6 +10055,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "devOptional": true, "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -10208,6 +10068,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -10218,12 +10079,14 @@ "node_modules/minizlib/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "devOptional": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -10341,49 +10204,10 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", - "devOptional": true, "engines": { "node": "^16 || ^18 || >= 20" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -10409,6 +10233,16 @@ "node": ">= 10.12.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-gyp/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -10480,6 +10314,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "optional": true, "dependencies": { "abbrev": "1" }, @@ -10741,6 +10577,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, "dependencies": { "wrappy": "1" } @@ -10958,6 +10795,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -11904,6 +11742,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "devOptional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -12259,6 +12098,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -12273,6 +12113,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -12282,6 +12123,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -12301,6 +12143,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12478,6 +12321,7 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "devOptional": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -12492,6 +12336,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -12502,7 +12347,8 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true }, "node_modules/send": { "version": "0.18.0", @@ -12549,7 +12395,9 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "optional": true }, "node_modules/set-cookie-parser": { "version": "2.6.0", @@ -12650,7 +12498,8 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "node_modules/simple-concat": { "version": "1.0.1", @@ -12947,6 +12796,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "devOptional": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -12955,6 +12805,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "devOptional": true, "funding": [ { "type": "github", @@ -13260,6 +13111,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "devOptional": true, "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -13320,6 +13172,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "devOptional": true, "dependencies": { "minipass": "^3.0.0" }, @@ -13331,6 +13184,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -13342,6 +13196,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "devOptional": true, "engines": { "node": ">=8" } @@ -13349,7 +13204,8 @@ "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true }, "node_modules/test-exclude": { "version": "6.0.0", @@ -14627,7 +14483,8 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true }, "node_modules/utils-merge": { "version": "1.0.1", @@ -15686,6 +15543,8 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "optional": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -15694,6 +15553,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "optional": true, "engines": { "node": ">=8" } @@ -15701,12 +15562,16 @@ "node_modules/wide-align/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "optional": true }, "node_modules/wide-align/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "optional": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -15720,6 +15585,8 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "optional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -15811,7 +15678,8 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true }, "node_modules/ws": { "version": "7.5.9", diff --git a/package.json b/package.json index 641ef79..903a5b5 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@shlinkio/shlink-frontend-kit": "^0.5.1", "@shlinkio/shlink-js-sdk": "^1.1.0", "@shlinkio/shlink-web-component": "^0.6.2", - "bcrypt": "^5.1.1", + "argon2": "^0.40.1", "bootstrap": "5.2.3", "bottlejs": "^2.0.1", "express": "^4.19.2", diff --git a/server.ts b/server.ts index 6d603dd..517a43b 100644 --- a/server.ts +++ b/server.ts @@ -7,12 +7,7 @@ const viteDevServer = isProd() : await import('vite').then( (vite) => vite.createServer({ - server: { - middlewareMode: true, - watch: { - ignored: ['**/home/**', '**/build/**', '**/.idea/**', '**/node_modules/**', '**/.git/**'], - }, - }, + server: { middlewareMode: true }, }), ); From 68aa8837a645bec6d3c9eb185b881672c64c7a42 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 15 May 2024 23:31:12 +0200 Subject: [PATCH 5/7] Add auth test --- app/auth/auth.server.ts | 10 ++--- app/auth/credentials-schema.server.ts | 6 +++ app/db/data-source.server.ts | 2 +- app/routes/login.tsx | 10 ++--- test/auth/auth.server.test.ts | 53 +++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 app/auth/credentials-schema.server.ts create mode 100644 test/auth/auth.server.test.ts diff --git a/app/auth/auth.server.ts b/app/auth/auth.server.ts index 11914c9..ed12af5 100644 --- a/app/auth/auth.server.ts +++ b/app/auth/auth.server.ts @@ -2,6 +2,7 @@ import type { Strategy } from 'remix-auth'; import { Authenticator } from 'remix-auth'; import { FormStrategy } from 'remix-auth-form'; import type { UsersService } from '../users/UsersService.server'; +import { credentialsSchema } from './credentials-schema.server'; import type { SessionData, SessionStorage } from './session.server'; export const CREDENTIALS_STRATEGY = 'credentials'; @@ -11,11 +12,10 @@ function getAuthStrategies(usersService: UsersService): Map => { - const username = form.get('username'); - const password = form.get('password'); - if (typeof username !== 'string' || typeof password !== 'string') { - throw new Error('Username or password missing'); - } + const { username, password } = credentialsSchema.parse({ + username: form.get('username'), + password: form.get('password'), + }); const user = await usersService.getUserByCredentials(username, password); return { userId: user.id }; diff --git a/app/auth/credentials-schema.server.ts b/app/auth/credentials-schema.server.ts new file mode 100644 index 0000000..d1df86b --- /dev/null +++ b/app/auth/credentials-schema.server.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; + +export const credentialsSchema = z.object({ + username: z.string().min(1), + password: z.string().min(1), +}); diff --git a/app/db/data-source.server.ts b/app/db/data-source.server.ts index 9cd1e5d..46ab112 100644 --- a/app/db/data-source.server.ts +++ b/app/db/data-source.server.ts @@ -5,7 +5,7 @@ import { SettingsEntity } from '../entities/Settings'; import { TagEntity } from '../entities/Tag'; import { UserEntity } from '../entities/User'; import type { DbEngine } from '../utils/env.server'; -import { env, isProd } from '../utils/env.server'; +import { env } from '../utils/env.server'; const DEFAULT_PORTS: Record, number | undefined> = { mysql: 3306, diff --git a/app/routes/login.tsx b/app/routes/login.tsx index 01b7d99..7a642e5 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -29,7 +29,7 @@ export async function loader( const session = await getSession(request.headers.get('cookie')); const error = session.get(authenticator.sessionErrorKey); - return json({ hasError: !!error }, { + return json({ error }, { headers: { 'Set-Cookie': await commitSession(session), }, @@ -39,7 +39,7 @@ export async function loader( export default function Login() { const usernameId = useId(); const passwordId = useId(); - const { hasError } = useLoaderData(); + const { error } = useLoaderData(); return (
- +
- +
- {hasError &&
Username or password are incorrect
} + {!!error &&
Username or password are incorrect
}
); } diff --git a/test/auth/auth.server.test.ts b/test/auth/auth.server.test.ts new file mode 100644 index 0000000..ec5d491 --- /dev/null +++ b/test/auth/auth.server.test.ts @@ -0,0 +1,53 @@ +import { fromPartial } from '@total-typescript/shoehorn'; +import { ReadableStream } from 'node:stream/web'; +import { Authenticator } from 'remix-auth'; +import { createAuthenticator, CREDENTIALS_STRATEGY } from '../../app/auth/auth.server'; +import type { UsersService } from '../../app/users/UsersService.server'; + +describe('auth', () => { + const getUserByCredentials = vi.fn(); + const usersService = fromPartial({ getUserByCredentials }); + + describe('createAuthenticator', () => { + it('creates the authenticator instance', () => { + const authenticator = createAuthenticator(usersService, fromPartial({})); + expect(authenticator).toBeInstanceOf(Authenticator); + }); + }); + + describe('authenticator', () => { + const authenticator = createAuthenticator(usersService, fromPartial({})); + const requestWithBody = (body: string = '') => { + const headers = new Headers(); + headers.set('Content-Type', 'application/x-www-form-urlencoded'); + return fromPartial({ + body: ReadableStream.from(body), + url: 'https://example.com', + headers, + method: 'POST', + }); + }; + + it('throws error when credentials are invalid', async () => { + const callback = () => authenticator.authenticate( + CREDENTIALS_STRATEGY, + requestWithBody(), + ); + + await expect(callback).rejects.toThrow(); + expect(getUserByCredentials).not.toHaveBeenCalled(); + }); + + it('tries to find user with credentials', async () => { + getUserByCredentials.mockResolvedValue({ id: 123 }); + + const result = await authenticator.authenticate( + CREDENTIALS_STRATEGY, + requestWithBody('username=foo&password=bar'), + ); + + expect(result).toEqual({ userId: 123 }); + expect(getUserByCredentials).toHaveBeenCalledWith('foo', 'bar'); + }); + }); +}); From 1a3bc68289e1f733a91e855da774009fe7299283 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 15 May 2024 23:39:09 +0200 Subject: [PATCH 6/7] Create session test --- test/auth/session.server.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 test/auth/session.server.test.ts diff --git a/test/auth/session.server.test.ts b/test/auth/session.server.test.ts new file mode 100644 index 0000000..fd3d46a --- /dev/null +++ b/test/auth/session.server.test.ts @@ -0,0 +1,11 @@ +import { createSessionStorage } from '../../app/auth/session.server'; + +describe('createSessionStorage', () => { + it('creates a session storage', () => { + const storage = createSessionStorage(); + + expect(storage.destroySession).toEqual(expect.any(Function)); + expect(storage.commitSession).toEqual(expect.any(Function)); + expect(storage.getSession).toEqual(expect.any(Function)); + }); +}); From 05dc13eea57d54777b32fb3db1c1539cda98e349 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Thu, 16 May 2024 09:33:39 +0200 Subject: [PATCH 7/7] Add login route test --- app/routes/login.tsx | 2 +- test/routes/login.test.tsx | 78 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 test/routes/login.test.tsx diff --git a/app/routes/login.tsx b/app/routes/login.tsx index 7a642e5..7b4ca52 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -56,7 +56,7 @@ export default function Login() { - {!!error &&
Username or password are incorrect
} + {!!error &&
Username or password are incorrect
} ); } diff --git a/test/routes/login.test.tsx b/test/routes/login.test.tsx new file mode 100644 index 0000000..8c484f3 --- /dev/null +++ b/test/routes/login.test.tsx @@ -0,0 +1,78 @@ +import type { Session } from '@remix-run/node'; +import { json } from '@remix-run/node'; +import { createRemixStub } from '@remix-run/testing'; +import { render, screen, waitFor } from '@testing-library/react'; +import { fromPartial } from '@total-typescript/shoehorn'; +import type { Authenticator } from 'remix-auth'; +import { CREDENTIALS_STRATEGY } from '../../app/auth/auth.server'; +import type { SessionStorage } from '../../app/auth/session.server'; +import Login, { action, loader } from '../../app/routes/login'; + +describe('login', () => { + const authenticate = vi.fn(); + const isAuthenticated = vi.fn().mockResolvedValue(undefined); + const authenticator = fromPartial({ authenticate, isAuthenticated }); + const getSession = vi.fn(); + const commitSession = vi.fn().mockResolvedValue(''); + const sessionStorage = fromPartial({ getSession, commitSession }); + + describe('action', () => { + it.each([ + ['http://example.com', '/'], + [`http://example.com?redirect-to=${encodeURIComponent('/foo/bar')}`, '/foo/bar'], + ])('authenticates user and redirects to expected location', (url, expectedSuccessRedirect) => { + const request = fromPartial({ url }); + action(fromPartial({ request }), authenticator); + + expect(authenticate).toHaveBeenCalledWith(CREDENTIALS_STRATEGY, request, { + successRedirect: expectedSuccessRedirect, + failureRedirect: url, + }); + }); + }); + + describe('loader', () => { + it('checks authentication and exposes error from session, if any', async () => { + const error = 'the_error'; + const session = fromPartial({ get: vi.fn().mockReturnValue(error) }); + getSession.mockResolvedValue(session); + + const headers = new Headers(); + headers.set('cookie', 'the_cookies'); + const request = fromPartial({ headers }); + + const response = await loader(fromPartial({ request }), authenticator, sessionStorage); + + expect(await response.json()).toEqual({ error }); + expect(isAuthenticated).toHaveBeenCalled(); + expect(getSession).toHaveBeenCalledWith('the_cookies'); + expect(commitSession).toHaveBeenCalledWith(session); + }); + }); + + describe('', () => { + const setUp = (error: unknown = undefined) => { + const RemixStub = createRemixStub([ + { + path: '/', + Component: Login, + loader: () => json({ error }), + }, + ]); + return render(); + }; + + it('renders expected form controls', async () => { + setUp(); + + await waitFor(() => expect(screen.getByLabelText('Username:')).toBeInTheDocument()); + expect(screen.getByLabelText('Password:')).toBeInTheDocument(); + expect(screen.queryByTestId('error-message')).not.toBeInTheDocument(); + }); + + it('renders error when present', async () => { + setUp('some error'); + await waitFor(() => expect(screen.getByTestId('error-message')).toBeInTheDocument()); + }); + }); +});