From cef472ce67a9fe78c86ecc5cca622b134d51cc18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Sun, 15 Mar 2026 17:56:30 +0100 Subject: [PATCH] feat: use @adonisjs/auth with custom MikroORM user provider Replace custom JWT authentication with AdonisJS's built-in auth system using a SessionUserProviderContract backed by MikroORM. This provides proper framework integration with session guards, route-level auth middleware, and loginAs test helpers. Co-Authored-By: Claude Opus 4.6 (1M context) --- adonisrc.ts | 2 + app/auth/mikro_orm_user_provider.ts | 36 ++++ app/controllers/articles_controller.ts | 29 ++- app/controllers/users_controller.ts | 21 +- app/entities/user.ts | 1 - app/exceptions/handler.ts | 8 +- app/middleware/auth_middleware.ts | 27 +-- app/services/jwt.ts | 42 ---- app/types.ts | 7 - app/utils/auth.ts | 11 - config/auth.ts | 25 +++ config/session.ts | 13 ++ package-lock.json | 288 ++++++++++++++++++------- package.json | 5 +- start/kernel.ts | 6 +- start/routes.ts | 21 +- tests/bootstrap.ts | 10 +- tests/functional/article.spec.ts | 42 ++-- tests/functional/user.spec.ts | 18 +- 19 files changed, 380 insertions(+), 232 deletions(-) create mode 100644 app/auth/mikro_orm_user_provider.ts delete mode 100644 app/services/jwt.ts delete mode 100644 app/types.ts delete mode 100644 app/utils/auth.ts create mode 100644 config/auth.ts create mode 100644 config/session.ts diff --git a/adonisrc.ts b/adonisrc.ts index a01a6b4..ee6fad3 100644 --- a/adonisrc.ts +++ b/adonisrc.ts @@ -6,6 +6,8 @@ export default defineConfig({ providers: [ () => import('@adonisjs/core/providers/app_provider'), () => import('@adonisjs/core/providers/hash_provider'), + () => import('@adonisjs/session/session_provider'), + () => import('@adonisjs/auth/auth_provider'), { file: () => import('@adonisjs/core/providers/repl_provider'), environment: ['repl', 'test'], diff --git a/app/auth/mikro_orm_user_provider.ts b/app/auth/mikro_orm_user_provider.ts new file mode 100644 index 0000000..bf7fd19 --- /dev/null +++ b/app/auth/mikro_orm_user_provider.ts @@ -0,0 +1,36 @@ +import { symbols } from '@adonisjs/auth' +import type { SessionGuardUser, SessionUserProviderContract } from '@adonisjs/auth/types/session' +import type { EntityManager } from '@mikro-orm/sqlite' +import { User } from '#entities/user' + +/** + * Bridges MikroORM with the AdonisJS session guard. + * Implements SessionUserProviderContract so the auth system + * can look up users via MikroORM's EntityManager. + */ +export class MikroOrmUserProvider implements SessionUserProviderContract { + declare [symbols.PROVIDER_REAL_USER]: User + + constructor(private em: EntityManager) {} + + async createUserForGuard(user: User): Promise> { + return { + getId() { + return user.id + }, + getOriginal() { + return user + }, + } + } + + async findById(identifier: number): Promise | null> { + const user = await this.em.findOne(User, identifier) + + if (!user) { + return null + } + + return this.createUserForGuard(user) + } +} diff --git a/app/controllers/articles_controller.ts b/app/controllers/articles_controller.ts index 01cf14f..9fc6b61 100644 --- a/app/controllers/articles_controller.ts +++ b/app/controllers/articles_controller.ts @@ -5,7 +5,6 @@ import { EntityManager } from '@mikro-orm/sqlite' import { CommentSchema } from '#entities/comment' import { AuthError } from '#repositories/user_repository' import { ArticleRepository } from '#repositories/article_repository' -import { getUserFromCtx } from '#utils/auth' import { type IArticle } from '#entities/article' function verifyArticlePermissions(user: { id: number }, article: IArticle) { @@ -38,9 +37,9 @@ export default class ArticlesController { }) } - async store(ctx: HttpContext) { - const author = getUserFromCtx(ctx) - const { title, description, text } = ctx.request.body() + async store({ auth, request }: HttpContext) { + const author = auth.getUserOrFail() + const { title, description, text } = request.body() const article = this.articleRepo.create({ title, description, text, @@ -52,11 +51,11 @@ export default class ArticlesController { return article } - async update(ctx: HttpContext) { - const user = getUserFromCtx(ctx) - const article = await this.articleRepo.findOneOrFail(+ctx.params.id) + async update({ auth, params, request }: HttpContext) { + const user = auth.getUserOrFail() + const article = await this.articleRepo.findOneOrFail(+params.id) verifyArticlePermissions(user, article) - const body = ctx.request.body() + const body = request.body() const data: Record = {} for (const key of ['title', 'description', 'text'] as const) { @@ -71,19 +70,19 @@ export default class ArticlesController { return article } - async destroy(ctx: HttpContext) { - const user = getUserFromCtx(ctx) - const article = await this.articleRepo.findOneOrFail(+ctx.params.id) + async destroy({ auth, params }: HttpContext) { + const user = auth.getUserOrFail() + const article = await this.articleRepo.findOneOrFail(+params.id) verifyArticlePermissions(user, article) await this.em.remove(article).flush() return { success: true } } - async addComment(ctx: HttpContext) { - const author = getUserFromCtx(ctx) - const article = await this.articleRepo.findOneOrFail({ slug: ctx.params.slug }) - const { text } = ctx.request.body() + async addComment({ auth, params, request }: HttpContext) { + const author = auth.getUserOrFail() + const article = await this.articleRepo.findOneOrFail({ slug: params.slug }) + const { text } = request.body() const comment = this.em.create(CommentSchema, { author, article, text }) await this.em.flush() diff --git a/app/controllers/users_controller.ts b/app/controllers/users_controller.ts index 530b674..90261d0 100644 --- a/app/controllers/users_controller.ts +++ b/app/controllers/users_controller.ts @@ -2,9 +2,7 @@ import { inject } from '@adonisjs/core' import type { HttpContext } from '@adonisjs/core/http' import { wrap } from '@mikro-orm/core' import { EntityManager } from '@mikro-orm/sqlite' -import { signJwt } from '#services/jwt' import { AuthError, UserRepository } from '#repositories/user_repository' -import { getUserFromCtx } from '#utils/auth' @inject() export default class UsersController { @@ -13,7 +11,7 @@ export default class UsersController { protected userRepo: UserRepository, ) {} - async signUp({ request, response }: HttpContext) { + async signUp({ request, response, auth }: HttpContext) { const { fullName, email, password, bio } = request.body() if (await this.userRepo.exists(email)) { @@ -24,18 +22,17 @@ export default class UsersController { const user = this.userRepo.create({ fullName, email, password, bio }) await this.em.flush() - - user.token = signJwt({ id: user.id }) + await auth.use('web').login(user) return user } - async signIn({ request, response }: HttpContext) { + async signIn({ request, response, auth }: HttpContext) { const { email, password } = request.body() try { const user = await this.userRepo.login(email, password) - user.token = signJwt({ id: user.id }) + await auth.use('web').login(user) return user } catch (error) { if (error instanceof AuthError) { @@ -46,13 +43,13 @@ export default class UsersController { } } - async profile(ctx: HttpContext) { - return getUserFromCtx(ctx) + async profile({ auth }: HttpContext) { + return auth.getUserOrFail() } - async updateProfile(ctx: HttpContext) { - const user = getUserFromCtx(ctx) - const body = ctx.request.body() + async updateProfile({ auth, request }: HttpContext) { + const user = auth.getUserOrFail() + const body = request.body() const data: Record = {} for (const key of ['fullName', 'bio', 'social'] as const) { diff --git a/app/entities/user.ts b/app/entities/user.ts index 641e290..4a2133c 100644 --- a/app/entities/user.ts +++ b/app/entities/user.ts @@ -39,7 +39,6 @@ export const UserSchema = defineEntity({ password: p.string().hidden().lazy(), bio: p.text().default(''), articles: () => p.oneToMany(ArticleSchema).mappedBy('author'), - token: p.string().persist(false).nullable(), social: () => p.embedded(SocialSchema).object().nullable(), }, }) diff --git a/app/exceptions/handler.ts b/app/exceptions/handler.ts index 92788c2..59fc653 100644 --- a/app/exceptions/handler.ts +++ b/app/exceptions/handler.ts @@ -1,5 +1,6 @@ import app from '@adonisjs/core/services/app' import { HttpContext, ExceptionHandler } from '@adonisjs/core/http' +import { errors as authErrors } from '@adonisjs/auth' import { NotFoundError } from '@mikro-orm/core' import { AuthError } from '#repositories/user_repository' @@ -7,6 +8,11 @@ export default class HttpExceptionHandler extends ExceptionHandler { protected debug = !app.inProduction async handle(error: unknown, ctx: HttpContext) { + if (error instanceof authErrors.E_UNAUTHORIZED_ACCESS) { + ctx.response.status(401).send({ error: 'Unauthorized' }) + return + } + if (error instanceof AuthError) { ctx.response.status(401).send({ error: error.message }) return @@ -21,7 +27,7 @@ export default class HttpExceptionHandler extends ExceptionHandler { } async report(error: unknown, ctx: HttpContext) { - if (error instanceof AuthError) { + if (error instanceof authErrors.E_UNAUTHORIZED_ACCESS || error instanceof AuthError) { return } diff --git a/app/middleware/auth_middleware.ts b/app/middleware/auth_middleware.ts index 565d78b..02e5519 100644 --- a/app/middleware/auth_middleware.ts +++ b/app/middleware/auth_middleware.ts @@ -1,31 +1,14 @@ -import { inject } from '@adonisjs/core' import type { HttpContext } from '@adonisjs/core/http' import type { NextFn } from '@adonisjs/core/types/http' -import { UserRepository } from '#repositories/user_repository' -import { verifyJwt } from '#services/jwt' +import type { Authenticators } from '@adonisjs/auth/types' /** - * Attempts to authenticate the user from the Authorization header. - * Does not reject unauthenticated requests — just sets `ctx.user` if valid. + * Protects routes from unauthenticated users. Throws E_UNAUTHORIZED_ACCESS + * when the user is not logged in. */ -@inject() export default class AuthMiddleware { - constructor(protected userRepo: UserRepository) {} - - async handle(ctx: HttpContext, next: NextFn) { - const header = ctx.request.header('authorization') - - if (header?.startsWith('Bearer ')) { - const token = header.slice(7) - - try { - const payload = verifyJwt(token) - ctx.user = await this.userRepo.findOneOrFail(payload.id) - } catch { - // ignore invalid tokens, we validate ctx.user where needed - } - } - + async handle(ctx: HttpContext, next: NextFn, options: { guards?: (keyof Authenticators)[] } = {}) { + await ctx.auth.authenticateUsing(options.guards || [ctx.auth.defaultGuard]) return next() } } diff --git a/app/services/jwt.ts b/app/services/jwt.ts deleted file mode 100644 index 8fdcce6..0000000 --- a/app/services/jwt.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { createHmac, timingSafeEqual } from 'node:crypto' - -const SECRET = process.env.JWT_SECRET ?? '12345678' -const TOKEN_MAX_AGE = 60 * 60 * 24 // 24 hours in seconds - -interface JwtPayload { - id: number - iat: number -} - -function base64url(data: string): string { - return Buffer.from(data).toString('base64url') -} - -export function signJwt(payload: { id: number }): string { - const header = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })) - const body = base64url(JSON.stringify({ ...payload, iat: Math.floor(Date.now() / 1000) })) - const signature = createHmac('sha256', SECRET) - .update(`${header}.${body}`) - .digest('base64url') - return `${header}.${body}.${signature}` -} - -export function verifyJwt(token: string): JwtPayload { - const [header, body, signature] = token.split('.') - const expected = createHmac('sha256', SECRET) - .update(`${header}.${body}`) - .digest('base64url') - - if (!timingSafeEqual(Buffer.from(signature!), Buffer.from(expected))) { - throw new Error('Invalid token signature') - } - - const payload: JwtPayload = JSON.parse(Buffer.from(body!, 'base64url').toString()) - const age = Math.floor(Date.now() / 1000) - payload.iat - - if (age > TOKEN_MAX_AGE) { - throw new Error('Token expired') - } - - return payload -} diff --git a/app/types.ts b/app/types.ts deleted file mode 100644 index 04eb8dc..0000000 --- a/app/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { User } from '#entities/user' - -declare module '@adonisjs/core/http' { - interface HttpContext { - user?: User - } -} diff --git a/app/utils/auth.ts b/app/utils/auth.ts deleted file mode 100644 index f519225..0000000 --- a/app/utils/auth.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { HttpContext } from '@adonisjs/core/http' -import { type User } from '#entities/user' -import { AuthError } from '#repositories/user_repository' - -export function getUserFromCtx(ctx: HttpContext): User { - if (!ctx.user) { - throw new AuthError('Please provide your token via Authorization header') - } - - return ctx.user as User -} diff --git a/config/auth.ts b/config/auth.ts new file mode 100644 index 0000000..5c6f8e8 --- /dev/null +++ b/config/auth.ts @@ -0,0 +1,25 @@ +import { defineConfig } from '@adonisjs/auth' +import { sessionGuard } from '@adonisjs/auth/session' +import { configProvider } from '@adonisjs/core' +import { EntityManager } from '@mikro-orm/sqlite' +import type { InferAuthenticators } from '@adonisjs/auth/types' + +const authConfig = defineConfig({ + default: 'web', + guards: { + web: sessionGuard({ + useRememberMeTokens: false, + provider: configProvider.create(async (app) => { + const { MikroOrmUserProvider } = await import('#auth/mikro_orm_user_provider') + const em = await app.container.make(EntityManager) + return new MikroOrmUserProvider(em) + }), + }), + }, +}) + +export default authConfig + +declare module '@adonisjs/auth/types' { + interface Authenticators extends InferAuthenticators {} +} diff --git a/config/session.ts b/config/session.ts new file mode 100644 index 0000000..6164961 --- /dev/null +++ b/config/session.ts @@ -0,0 +1,13 @@ +import app from '@adonisjs/core/services/app' +import { defineConfig, stores } from '@adonisjs/session' + +const sessionConfig = defineConfig({ + age: '2h', + store: app.inTest ? 'memory' : 'cookie', + cookie: {}, + stores: { + cookie: stores.cookie(), + }, +}) + +export default sessionConfig diff --git a/package-lock.json b/package-lock.json index 6b82701..ecc78c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@adonisjs/auth": "^10.0.0", "@adonisjs/core": "^7.0.1", + "@adonisjs/session": "^8.0.0", "@mikro-orm/core": "^7.0.1", "@mikro-orm/migrations": "^7.0.1", "@mikro-orm/sqlite": "^7.0.1", @@ -117,6 +119,52 @@ "typescript": "^4.0.0 || ^5.0.0" } }, + "node_modules/@adonisjs/auth": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@adonisjs/auth/-/auth-10.0.0.tgz", + "integrity": "sha512-ag78Yhrwi+ljsQKXZFwe5Qpkbt6az8NqWvU2NZSVVzP/JWwp5GShzX6DOAzVQIhbViBnLB66xnkL7qnRd62tdA==", + "license": "MIT", + "dependencies": { + "@adonisjs/presets": "^3.0.0", + "basic-auth": "^2.0.1" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.26 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.16 || ^7.0.0", + "@adonisjs/i18n": "^3.0.0-next.2 || ^3.0.0", + "@adonisjs/lucid": "^22.0.0-next.1 || ^22.0.0", + "@adonisjs/session": "^8.0.0-next.1 || ^8.0.0", + "@japa/api-client": "^3.1.1", + "@japa/browser-client": "^2.2.0", + "@japa/plugin-adonisjs": "^5.1.0-next.0 || ^5.1.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + }, + "@adonisjs/i18n": { + "optional": true + }, + "@adonisjs/lucid": { + "optional": true + }, + "@adonisjs/session": { + "optional": true + }, + "@japa/api-client": { + "optional": true + }, + "@japa/browser-client": { + "optional": true + }, + "@japa/plugin-adonisjs": { + "optional": true + } + } + }, "node_modules/@adonisjs/bodyparser": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/@adonisjs/bodyparser/-/bodyparser-11.0.0.tgz", @@ -385,6 +433,24 @@ } } }, + "node_modules/@adonisjs/presets": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@adonisjs/presets/-/presets-3.0.0.tgz", + "integrity": "sha512-+gVIvyEiM7jiN5Gb93200TAC8ed3vZIPfxFFo0pIVgX8k40BleuYhWxFhI6TPocVXXIIpw2JuMFV2Pqjk36W2A==", + "license": "MIT", + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.9 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.1 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + } + } + }, "node_modules/@adonisjs/repl": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@adonisjs/repl/-/repl-5.0.0.tgz", @@ -398,6 +464,60 @@ "node": ">=24.0.0" } }, + "node_modules/@adonisjs/session": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@adonisjs/session/-/session-8.0.0.tgz", + "integrity": "sha512-JPPhZG3shdNKkMl8J9Gt9UVxk1nwzvaKctIzj1T2gTP4UwsTncX6eXk9djK6xU2mgP3ipxGKI5chpfPnW95poA==", + "license": "MIT", + "dependencies": { + "@poppinss/macroable": "^1.1.0", + "@poppinss/utils": "^7.0.0" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@adonisjs/assembler": "^8.0.0-next.26 || ^8.0.0", + "@adonisjs/core": "^7.0.0-next.16 || ^7.0.0", + "@adonisjs/lucid": "^22.0.0-next.0 || ^22.0.0", + "@adonisjs/redis": "^10.0.0-next.2 || ^10.0.0", + "@aws-sdk/client-dynamodb": "^3.955.0", + "@aws-sdk/util-dynamodb": "^3.955.0", + "@japa/api-client": "^3.1.0", + "@japa/browser-client": "^2.0.3", + "@japa/plugin-adonisjs": "^5.1.0-next.0 || ^5.1.0", + "edge.js": "^6.4.0" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + }, + "@adonisjs/lucid": { + "optional": true + }, + "@adonisjs/redis": { + "optional": true + }, + "@aws-sdk/client-dynamodb": { + "optional": true + }, + "@aws-sdk/util-dynamodb": { + "optional": true + }, + "@japa/api-client": { + "optional": true + }, + "@japa/browser-client": { + "optional": true + }, + "@japa/plugin-adonisjs": { + "optional": true + }, + "edge.js": { + "optional": true + } + } + }, "node_modules/@adonisjs/tsconfig": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@adonisjs/tsconfig/-/tsconfig-2.0.0.tgz", @@ -660,7 +780,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/@japa/api-client/-/api-client-3.2.1.tgz", "integrity": "sha512-dICbeEkgGRpjkm3CviOpvPfYMBZddaoW2w20pgNMm3CfvD2bixWOFn6FBRZRq5L+fHYe/O/xfSNGMCQBFy7Zlw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@poppinss/hooks": "^7.3.0", @@ -692,7 +812,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/@japa/assert/-/assert-4.2.0.tgz", "integrity": "sha512-Krgrcee01BN1StlVwK5JQP6LL5t3DE3uFNbfFoDTfW7kQuHB0xh6yfaV0hrgcoiEjsqmm2OOsVWeju9aXK4vIA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@poppinss/macroable": "^1.1.0", @@ -711,7 +831,7 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@japa/core/-/core-10.4.0.tgz", "integrity": "sha512-1zvKL29i7r/4jqTNBsw91hk1tp6wbqFXvyV2p+Og4axDRhIXjSAfauRvBL9QuB80bOa+pDIMQ5kCTaCplSm0Eg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@poppinss/hooks": "^7.3.0", @@ -729,7 +849,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/@japa/errors-printer/-/errors-printer-4.1.4.tgz", "integrity": "sha512-ogPT87QLaugKyPVcq+ZypcetVeJpXZN7RfVRlRDIrSHsHBw6ILCtNXghVxD9POjqxtUM7RON3sUkgZzLnVQA5g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@poppinss/colors": "^4.1.6", @@ -745,7 +865,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@japa/plugin-adonisjs/-/plugin-adonisjs-5.1.0.tgz", "integrity": "sha512-KTbpxdyyJHvplvXXJDVqCwFXPUe+QCiWMZQOklMhafzMWU0lAPEgVla3esUTgN0NQU/m9m+a/B6XUTEu5VYDIg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18.16.0" @@ -773,7 +893,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/@japa/runner/-/runner-5.3.0.tgz", "integrity": "sha512-WCnTd1q2EpbKKa96NzL16kVxJXVLRj1VqbswNAn17hYSuMlNKKhPGNbAosB32QZVFcoe9fv4Ebh1HtjyAT/viw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@japa/core": "^10.4.0", @@ -796,7 +916,7 @@ "version": "30.3.0", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -806,7 +926,7 @@ "version": "30.1.0", "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -816,7 +936,7 @@ "version": "30.0.5", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.34.0" @@ -927,7 +1047,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -978,7 +1098,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@noble/hashes": "^1.1.5" @@ -1171,7 +1291,7 @@ "version": "0.34.48", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@sindresorhus/is": { @@ -1470,7 +1590,7 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/deep-eql": "*", @@ -1481,14 +1601,14 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/he": { @@ -1501,14 +1621,14 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/node": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -1531,7 +1651,7 @@ "version": "8.1.9", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/cookiejar": "^2.1.5", @@ -1583,7 +1703,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1615,14 +1735,14 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -1632,7 +1752,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "retry": "0.13.1" @@ -1642,7 +1762,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/atomic-sleep": { @@ -1684,6 +1804,24 @@ ], "license": "MIT" }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/better-sqlite3": { "version": "12.6.2", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", @@ -1797,7 +1935,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1811,7 +1949,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1840,7 +1978,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -1850,7 +1988,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -1867,7 +2005,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -2045,7 +2183,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2058,7 +2196,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/colorette": { @@ -2072,7 +2210,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -2085,14 +2223,14 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/component-emitter": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2130,7 +2268,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cross-env": { @@ -2277,7 +2415,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -2315,7 +2453,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "asap": "^2.0.0", @@ -2326,7 +2464,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2417,7 +2555,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2427,7 +2565,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2443,7 +2581,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2456,7 +2594,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2661,7 +2799,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/find-cache-directory/-/find-cache-directory-6.0.0.tgz", "integrity": "sha512-CvFd5ivA6HcSHbD+59P7CyzINHXzwhuQK8RY7CxJZtgDSAtRlHiCaQpZQ2lMR/WRyUIEmzUvL6G2AGurMfegZA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "common-path-prefix": "^3.0.0", @@ -2678,7 +2816,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -2700,7 +2838,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -2717,7 +2855,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -2727,7 +2865,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -2740,7 +2878,7 @@ "version": "3.5.4", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@paralleldrive/cuid2": "^2.2.2", @@ -2782,7 +2920,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2814,7 +2952,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -2852,7 +2990,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -2896,7 +3034,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/github-from-package": { @@ -2921,7 +3059,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2934,7 +3072,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -2944,7 +3082,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2957,7 +3095,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -2973,7 +3111,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3304,7 +3442,7 @@ "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.3.0", @@ -3433,7 +3571,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3462,7 +3600,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -3509,7 +3647,7 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -3691,7 +3829,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3935,7 +4073,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-8.0.0.tgz", "integrity": "sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "find-up-simple": "^1.0.0" @@ -4000,7 +4138,7 @@ "version": "30.3.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jest/schemas": "30.0.5", @@ -4015,7 +4153,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -4092,7 +4230,7 @@ "version": "6.15.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -4174,7 +4312,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/read-package-up": { @@ -4311,7 +4449,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 4" @@ -4433,7 +4571,7 @@ "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/setprototypeof": { @@ -4467,7 +4605,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4487,7 +4625,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4504,7 +4642,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -4523,7 +4661,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -4824,7 +4962,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "component-emitter": "^1.3.1", @@ -4919,7 +5057,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.3.1.tgz", "integrity": "sha512-LeQRS7/4JcC0PgdSFnfUiStQEdiuySlCj/5SJ18D+T1n9BoY7PxKFfCwLulpHXoLUFr67HxBddQdEX47lDGx1g==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tinyexec": { @@ -5081,7 +5219,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicorn-magic": { diff --git a/package.json b/package.json index 6368d0f..47a89f5 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,13 @@ "typecheck": "tsc --noEmit" }, "imports": { + "#auth/*": "./app/auth/*.js", "#controllers/*": "./app/controllers/*.js", "#entities/*": "./app/entities/*.js", "#exceptions/*": "./app/exceptions/*.js", "#middleware/*": "./app/middleware/*.js", "#repositories/*": "./app/repositories/*.js", - "#services/*": "./app/services/*.js", "#subscribers/*": "./app/subscribers/*.js", - "#utils/*": "./app/utils/*.js", "#providers/*": "./providers/*.js", "#database/*": "./database/*.js", "#start/*": "./start/*.js", @@ -31,7 +30,9 @@ "#tests/*": "./tests/*.js" }, "dependencies": { + "@adonisjs/auth": "^10.0.0", "@adonisjs/core": "^7.0.1", + "@adonisjs/session": "^8.0.0", "@mikro-orm/core": "^7.0.1", "@mikro-orm/migrations": "^7.0.1", "@mikro-orm/sqlite": "^7.0.1", diff --git a/start/kernel.ts b/start/kernel.ts index 9bf14a1..0852f2b 100644 --- a/start/kernel.ts +++ b/start/kernel.ts @@ -6,8 +6,12 @@ server.errorHandler(() => import('#exceptions/handler')) server.use([ () => import('#middleware/container_bindings_middleware'), () => import('#middleware/mikro_orm_middleware'), + () => import('@adonisjs/session/session_middleware'), + () => import('@adonisjs/auth/initialize_auth_middleware'), ]) router.use([() => import('@adonisjs/core/bodyparser_middleware')]) -router.use([() => import('#middleware/auth_middleware')]) +export const middleware = router.named({ + auth: () => import('#middleware/auth_middleware'), +}) diff --git a/start/routes.ts b/start/routes.ts index dd9b1a8..81e4223 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -1,18 +1,21 @@ import router from '@adonisjs/core/services/router' +import { middleware } from '#start/kernel' const UsersController = () => import('#controllers/users_controller') const ArticlesController = () => import('#controllers/articles_controller') -// user routes +// public routes router.post('/user/sign-up', [UsersController, 'signUp']) router.post('/user/sign-in', [UsersController, 'signIn']) -router.get('/user/profile', [UsersController, 'profile']) -router.patch('/user/profile', [UsersController, 'updateProfile']) - -// article routes router.get('/article', [ArticlesController, 'index']) router.get('/article/:slug', [ArticlesController, 'show']) -router.post('/article', [ArticlesController, 'store']) -router.patch('/article/:id', [ArticlesController, 'update']) -router.delete('/article/:id', [ArticlesController, 'destroy']) -router.post('/article/:slug/comment', [ArticlesController, 'addComment']) + +// protected routes +router.group(() => { + router.get('/user/profile', [UsersController, 'profile']) + router.patch('/user/profile', [UsersController, 'updateProfile']) + router.post('/article', [ArticlesController, 'store']) + router.patch('/article/:id', [ArticlesController, 'update']) + router.delete('/article/:id', [ArticlesController, 'destroy']) + router.post('/article/:slug/comment', [ArticlesController, 'addComment']) +}).use(middleware.auth()) diff --git a/tests/bootstrap.ts b/tests/bootstrap.ts index a4af8e9..3883efc 100644 --- a/tests/bootstrap.ts +++ b/tests/bootstrap.ts @@ -3,9 +3,17 @@ import { apiClient } from '@japa/api-client' import app from '@adonisjs/core/services/app' import type { Config } from '@japa/runner/types' import { pluginAdonisJS } from '@japa/plugin-adonisjs' +import { authApiClient } from '@adonisjs/auth/plugins/api_client' +import { sessionApiClient } from '@adonisjs/session/plugins/api_client' import testUtils from '@adonisjs/core/services/test_utils' -export const plugins: Config['plugins'] = [assert(), apiClient(), pluginAdonisJS(app)] +export const plugins: Config['plugins'] = [ + assert(), + apiClient(), + pluginAdonisJS(app), + sessionApiClient(app), + authApiClient(app), +] export const runnerHooks: Required> = { setup: [], diff --git a/tests/functional/article.spec.ts b/tests/functional/article.spec.ts index 8c31ff1..e370334 100644 --- a/tests/functional/article.spec.ts +++ b/tests/functional/article.spec.ts @@ -1,4 +1,12 @@ import { test } from '@japa/runner' +import app from '@adonisjs/core/services/app' +import { MikroORM } from '@mikro-orm/sqlite' +import { User } from '#entities/user' + +async function getTestUser() { + const orm = await app.container.make(MikroORM) + return orm.em.fork().findOneOrFail(User, { email: 'foo@bar.com' }) +} test.group('Article', () => { test('list all articles', async ({ client, assert }) => { @@ -29,17 +37,13 @@ test.group('Article', () => { }) response.assertStatus(401) - response.assertBodyContains({ error: 'Please provide your token via Authorization header' }) + response.assertBodyContains({ error: 'Unauthorized' }) }) test('create article with auth', async ({ client, assert }) => { - const signIn = await client.post('/user/sign-in').json({ - email: 'foo@bar.com', - password: 'password123', - }) - const token = signIn.body().token + const user = await getTestUser() - const response = await client.post('/article').header('authorization', `Bearer ${token}`).json({ + const response = await client.post('/article').loginAs(user).json({ title: 'Brand New Article', text: 'Some interesting content here', }) @@ -50,11 +54,7 @@ test.group('Article', () => { }) test('update article', async ({ client, assert }) => { - const signIn = await client.post('/user/sign-in').json({ - email: 'foo@bar.com', - password: 'password123', - }) - const token = signIn.body().token + const user = await getTestUser() // get the article ID via the detail endpoint const list = await client.get('/article') @@ -62,7 +62,7 @@ test.group('Article', () => { const detail = await client.get(`/article/${slug}`) const articleId = detail.body().id - const response = await client.patch(`/article/${articleId}`).header('authorization', `Bearer ${token}`).json({ + const response = await client.patch(`/article/${articleId}`).loginAs(user).json({ title: 'Updated Title', }) @@ -71,11 +71,7 @@ test.group('Article', () => { }) test('delete article', async ({ client, assert }) => { - const signIn = await client.post('/user/sign-in').json({ - email: 'foo@bar.com', - password: 'password123', - }) - const token = signIn.body().token + const user = await getTestUser() const list = await client.get('/article') const totalBefore = list.body().total @@ -83,7 +79,7 @@ test.group('Article', () => { const detail = await client.get(`/article/${slug}`) const articleId = detail.body().id - const response = await client.delete(`/article/${articleId}`).header('authorization', `Bearer ${token}`) + const response = await client.delete(`/article/${articleId}`).loginAs(user) response.assertStatus(200) response.assertBodyContains({ success: true }) @@ -93,16 +89,12 @@ test.group('Article', () => { }) test('add comment to article', async ({ client, assert }) => { - const signIn = await client.post('/user/sign-in').json({ - email: 'foo@bar.com', - password: 'password123', - }) - const token = signIn.body().token + const user = await getTestUser() const list = await client.get('/article') const slug = list.body().items[0].slug - const response = await client.post(`/article/${slug}/comment`).header('authorization', `Bearer ${token}`).json({ + const response = await client.post(`/article/${slug}/comment`).loginAs(user).json({ text: 'Great article!', }) diff --git a/tests/functional/user.spec.ts b/tests/functional/user.spec.ts index f2e4422..1b4e4dc 100644 --- a/tests/functional/user.spec.ts +++ b/tests/functional/user.spec.ts @@ -1,4 +1,7 @@ import { test } from '@japa/runner' +import app from '@adonisjs/core/services/app' +import { MikroORM } from '@mikro-orm/sqlite' +import { User } from '#entities/user' test.group('User', () => { test('sign in with valid credentials', async ({ client }) => { @@ -31,21 +34,20 @@ test.group('User', () => { signUpResponse.assertStatus(200) signUpResponse.assertBodyContains({ fullName: 'Test User' }) - const token = signUpResponse.body().token - const profileResponse = await client.get('/user/profile').header('authorization', `Bearer ${token}`) + // use loginAs with the newly created user + const orm = await app.container.make(MikroORM) + const user = await orm.em.fork().findOneOrFail(User, { email: 'test@example.com' }) + const profileResponse = await client.get('/user/profile').loginAs(user) profileResponse.assertStatus(200) profileResponse.assertBodyContains({ fullName: 'Test User' }) }) test('update profile', async ({ client, assert }) => { - const signIn = await client.post('/user/sign-in').json({ - email: 'foo@bar.com', - password: 'password123', - }) - const token = signIn.body().token + const orm = await app.container.make(MikroORM) + const user = await orm.em.fork().findOneOrFail(User, { email: 'foo@bar.com' }) - const response = await client.patch('/user/profile').header('authorization', `Bearer ${token}`).json({ + const response = await client.patch('/user/profile').loginAs(user).json({ bio: 'Updated bio text', })