diff --git a/.env.example b/.env.example
index 2cb243b..c1072d7 100644
--- a/.env.example
+++ b/.env.example
@@ -9,6 +9,7 @@ DB_PORT=5432
DB_HOST_PORT=5433
CORS_ORIGIN_ALLOWED=
LOGGER_TYPE=console
+JWT_SECRET=secret
TEST_DB_HOST=db_test
TEST_DB_NAME=test_db
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 21d453e..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-title: "[BUG] • "
-labels: bug
-assignees: ''
-
----
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To Reproduce**
-Steps to reproduce the behavior:
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**Operating system:**
-
-**Additional context**
-Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 45e0580..0000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-title: "[FEATURE] • "
-labels: feature
-assignees: ''
-
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/readme-improvement-or-correction.md b/.github/ISSUE_TEMPLATE/readme-improvement-or-correction.md
deleted file mode 100644
index 07ed724..0000000
--- a/.github/ISSUE_TEMPLATE/readme-improvement-or-correction.md
+++ /dev/null
@@ -1,11 +0,0 @@
----
-name: README improvement or correction
-about: Suggest a README improvement or correction
-title: "[README] • "
-labels: documentation
-assignees: ''
-
----
-
-**Describe what changes you would like**
-A clear and concise description of what you would like to change in the README
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
deleted file mode 100644
index 0242da9..0000000
--- a/.github/pull_request_template.md
+++ /dev/null
@@ -1,14 +0,0 @@
-## :speech_balloon: Describe your changes
-
-
-## :dna: Type of change
-- [ ] Bug fix
-- [ ] New feature
-- [ ] Documentation update
-- [ ] Packages upgrade
-
-## :white_check_mark: Checklist before requesting a review
-- [ ] I have performed a self-review of my code
-- [ ] I added the required tests (E2E/units)
-
-## :bulb: Additional informations
\ No newline at end of file
diff --git a/.github/workflows/main-tests.yml b/.github/workflows/main-tests.yml
deleted file mode 100644
index 39d13fc..0000000
--- a/.github/workflows/main-tests.yml
+++ /dev/null
@@ -1,70 +0,0 @@
-name: Update main code coverage
-run-name: Testing main to update code coverage 🧪
-
-on:
- push:
- branches:
- - main
- workflow_dispatch:
-
-env:
- DB_USER: postgres
- DB_PASSWORD: secret
- TEST_DB_HOST: localhost
- TEST_DB_NAME: test_db
- TEST_DB_PORT: 5432
-
-jobs:
- e2e-tests:
- name: E2E tests
- runs-on: ubuntu-latest
-
- services:
- postgres:
- image: postgres:15-alpine
- env:
- POSTGRES_PORT: ${{ env.TEST_DB_PORT }}
- POSTGRES_DB: ${{ env.TEST_DB_NAME }}
- POSTGRES_USER: ${{ env.DB_USER }}
- POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }}
- ports:
- - 5432:5432
- options: >-
- --health-cmd pg_isready
- --health-interval 10s
- --health-timeout 5s
- --health-retries 5
-
- steps:
- - name: Checkout repository code
- uses: actions/checkout@v3
-
- - name: Setup node version
- uses: actions/setup-node@v3
- with:
- node-version-file: '.node-version'
- cache: 'yarn'
-
- - name: Get yarn cache directory path
- id: yarn-cache-dir-path
- run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
-
- - name: Cache node modules
- uses: actions/cache@v4
- id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
- with:
- path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
- key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- restore-keys: |
- ${{ runner.os }}-yarn-
-
- - name: Install dependencies
- run: yarn install --frozen-lockfile
-
- - name: Run E2E tests
- run: yarn test:coverage
-
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v3
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 9734648..481bea0 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -19,6 +19,7 @@ env:
TEST_DB_HOST: localhost
TEST_DB_NAME: test_db
TEST_DB_PORT: 5432
+ JWT_SECRET: secret
jobs:
type-check-lint-and-build:
@@ -107,8 +108,3 @@ jobs:
- name: Run E2E tests
run: yarn test:coverage
-
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v3
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index 22421ce..0000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,128 +0,0 @@
-# Contributor Covenant Code of Conduct
-
-## Our Pledge
-
-We as members, contributors, and leaders pledge to make participation in our
-community a harassment-free experience for everyone, regardless of age, body
-size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
-nationality, personal appearance, race, religion, or sexual identity
-and orientation.
-
-We pledge to act and interact in ways that contribute to an open, welcoming,
-diverse, inclusive, and healthy community.
-
-## Our Standards
-
-Examples of behavior that contributes to a positive environment for our
-community include:
-
-* Demonstrating empathy and kindness toward other people
-* Being respectful of differing opinions, viewpoints, and experiences
-* Giving and gracefully accepting constructive feedback
-* Accepting responsibility and apologizing to those affected by our mistakes,
- and learning from the experience
-* Focusing on what is best not just for us as individuals, but for the
- overall community
-
-Examples of unacceptable behavior include:
-
-* The use of sexualized language or imagery, and sexual attention or
- advances of any kind
-* Trolling, insulting or derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or email
- address, without their explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
- professional setting
-
-## Enforcement Responsibilities
-
-Community leaders are responsible for clarifying and enforcing our standards of
-acceptable behavior and will take appropriate and fair corrective action in
-response to any behavior that they deem inappropriate, threatening, offensive,
-or harmful.
-
-Community leaders have the right and responsibility to remove, edit, or reject
-comments, commits, code, wiki edits, issues, and other contributions that are
-not aligned to this Code of Conduct, and will communicate reasons for moderation
-decisions when appropriate.
-
-## Scope
-
-This Code of Conduct applies within all community spaces, and also applies when
-an individual is officially representing the community in public spaces.
-Examples of representing our community include using an official e-mail address,
-posting via an official social media account, or acting as an appointed
-representative at an online or offline event.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported to the community leaders responsible for enforcement at
-dev.alexisleboucher@gmail.com.
-All complaints will be reviewed and investigated promptly and fairly.
-
-All community leaders are obligated to respect the privacy and security of the
-reporter of any incident.
-
-## Enforcement Guidelines
-
-Community leaders will follow these Community Impact Guidelines in determining
-the consequences for any action they deem in violation of this Code of Conduct:
-
-### 1. Correction
-
-**Community Impact**: Use of inappropriate language or other behavior deemed
-unprofessional or unwelcome in the community.
-
-**Consequence**: A private, written warning from community leaders, providing
-clarity around the nature of the violation and an explanation of why the
-behavior was inappropriate. A public apology may be requested.
-
-### 2. Warning
-
-**Community Impact**: A violation through a single incident or series
-of actions.
-
-**Consequence**: A warning with consequences for continued behavior. No
-interaction with the people involved, including unsolicited interaction with
-those enforcing the Code of Conduct, for a specified period of time. This
-includes avoiding interactions in community spaces as well as external channels
-like social media. Violating these terms may lead to a temporary or
-permanent ban.
-
-### 3. Temporary Ban
-
-**Community Impact**: A serious violation of community standards, including
-sustained inappropriate behavior.
-
-**Consequence**: A temporary ban from any sort of interaction or public
-communication with the community for a specified period of time. No public or
-private interaction with the people involved, including unsolicited interaction
-with those enforcing the Code of Conduct, is allowed during this period.
-Violating these terms may lead to a permanent ban.
-
-### 4. Permanent Ban
-
-**Community Impact**: Demonstrating a pattern of violation of community
-standards, including sustained inappropriate behavior, harassment of an
-individual, or aggression toward or disparagement of classes of individuals.
-
-**Consequence**: A permanent ban from any sort of public interaction within
-the community.
-
-## Attribution
-
-This Code of Conduct is adapted from the [Contributor Covenant][homepage],
-version 2.0, available at
-https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
-
-Community Impact Guidelines were inspired by [Mozilla's code of conduct
-enforcement ladder](https://github.com/mozilla/diversity).
-
-[homepage]: https://www.contributor-covenant.org
-
-For answers to common questions about this code of conduct, see the FAQ at
-https://www.contributor-covenant.org/faq. Translations are available at
-https://www.contributor-covenant.org/translations.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 8d66207..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# Contributing to the boilerplate
-
-Anyone and everyone is welcome to contribute.
-
-Please take a moment to review this document in order to make the contribution
-process easy and effective for everyone involved.
-
-Following these guidelines helps to communicate that you respect the time of
-the developers managing and developing this open source project. In return,
-they should reciprocate that respect in addressing your issue or assessing
-patches and features.
-
-## Issue report
-Before opening a pull request, please check if an [issue](https://github.com/alexleboucher/docker-express-postgres-boilerplate/issues) already exists. Otherwise, [open an issue](https://github.com/alexleboucher/docker-express-postgres-boilerplate/issues/new/choose).
-
-A good bug report shouldn't leave others needing to chase you up for more
-information. Please try to be as detailed as possible in your report. What is
-your environment? What steps will reproduce the issue? What would you expect
-to be the outcome? All these details will help people to fix any potential bugs.
-
-## Feature requests
-
-Feature requests are welcome. But take a moment to find out whether your idea
-fits with the scope and aims of the project. It's up to _you_ to make a strong
-case to convince the project's developers of the merits of this feature. Please
-provide as much detail and context as possible.
-
-## Pull requests
-
-Good pull requests - patches, improvements, new features - are a fantastic
-help. They should remain focused in scope and avoid containing unrelated
-commits.
-
-**Please ask first** before embarking on any significant pull request (e.g.
-implementing features, refactoring code, porting to a different language),
-otherwise you risk spending a lot of time working on something that the
-project's developers might not want to merge into the project.
-
-Please adhere to the coding conventions used throughout a project (indentation,
-accurate comments, etc.) and any other requirements (such as test coverage).
-
-
-
-**IMPORTANT**: By submitting a patch, you agree to allow the project
-owners to license your work under the terms of the [MIT License](LICENSE.txt).
\ No newline at end of file
diff --git a/README.md b/README.md
index 308c836..e1f2764 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
- A modern boilerplate for building scalable and maintainable REST APIs with authentication, written in TypeScript. It features Docker, Express, TypeORM, Passport, and integrates Clean Architecture principles with Dependency Injection powered by Inversify.
+ A modern boilerplate for building scalable and maintainable REST APIs with authentication, written in TypeScript. It features Docker, Express, TypeORM, jsonwebtoken for authentication by JWT, and integrates Clean Architecture principles with Dependency Injection powered by Inversify.
Made with ❤️ by Alex Le Boucher and contributors
@@ -35,7 +35,7 @@ It integrates common features such as:
- Docker containerization
- Database connection (PostgreSQL with TypeORM)
-- Authentication (using Passport)
+- Authentication (using jsonwebtoken)
- Centralized error handling
- Clean Architecture principles for better separation of concerns
- Dependency Injection powered by Inversify for modular and testable code
@@ -54,8 +54,7 @@ Packages are frequently upgraded. You can easily see the packages version status
## Features
- **Docker containerization** to easily run your code anywhere and avoid installing tools like PostgreSQL on your computer.
-- **Authentication** with [Passport](https://www.passportjs.org/).
-- **Authentication session** thanks to [express-session](https://github.com/expressjs/session) and [connect-pg-simple](https://github.com/voxpelli/node-connect-pg-simple).
+- **Authentication by JWT** with [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken).
- **Simplified Database Query** managed by [TypeORM](https://github.com/typeorm/typeorm).
- **Object-oriented database model** with [TypeORM](https://github.com/typeorm/typeorm) entities.
- **Integrated Testing Tools** with [Jest](https://jestjs.io/fr/docs/getting-started).
@@ -211,7 +210,6 @@ The project contains Github templates and workflows. If you don't want to keep t
| GET | `/health` | Retures the server health status | None. |
| POST | `/users` | Creates a new user. | `username` (min. 5 chars), `email` (valid), `password` (min. 8 chars). |
| POST | `/auth/login` | Logs in a user. | `email` and `password`. |
-| POST | `/auth/logout` | Logs out the currently authenticated user. | None. |
| GET | `/auth/authenticated` | Returns the user authentication status | None. |
---
@@ -237,7 +235,7 @@ The project contains Github templates and workflows. If you don't want to keep t
| **src/domain/services/** | Interfaces for domain-level services (e.g., authentication, encryption). |
| **src/domain/use-cases/** | Use cases implementing business logic. |
| **src/infra/** | Infrastructure layer providing implementations for core and domain abstractions. |
-| **src/infra/auth/** | Authentication implementations using Passport.js and session management. |
+| **src/infra/auth/** | Authentication implementations |
| **src/infra/database/** | Database configuration, models, and migrations. |
| **src/infra/database/repositories/** | Concrete implementations of domain repository interfaces using TypeORM. |
| **src/infra/id-generator/** | UUID-based ID generator. |
@@ -267,14 +265,9 @@ The project contains Github templates and workflows. If you don't want to keep t
| TEST_DB_NAME | Test database name. | ❌ | |
| TEST_DB_PORT | Test database host port. | ❌ | |
| TEST_DB_HOST_PORT | Test database mapped port for accessing the test database in Docker. | ❌ | |
+| JWT_SECRET | Secret used to encryot JSON web tokens. | ❌ | |
+| JWT_EXPIRES_IN_SECONDS | Number of seconds before JWT tokens expire. | ✔️ | 86400 |
| CORS_ORIGIN_ALLOWED | List of allowed origins for CORS. | ✔️ | * |
-| SESSION_SECRET | Secret key for signing the session ID cookie. | ✔️ | session-secret |
-| SESSION_RESAVE | Forces the session to be saved back to the session store, even if it was never modified. | ✔️ | false |
-| SESSION_SAVE_UNINITIALIZED | Forces an uninitialized session to be saved to the store. | ✔️ | false |
-| SESSION_COOKIE_SECURE | Ensures the cookie is only sent over HTTPS. | ✔️ | false |
-| SESSION_COOKIE_MAX_AGE | Lifetime of the session cookie in milliseconds. | ✔️ | 7776000000 (90 days) |
-| SESSION_COOKIE_HTTP_ONLY | Ensures the cookie is inaccessible to JavaScript (for XSS protection). | ✔️ | false |
-| SESSION_COOKIE_SAME_SITE | Controls whether the cookie is sent with cross-site requests. | ✔️ | lax |
| DB_LOGGING | Enables or disables query logging in TypeORM. | ✔️ | false |
| TYPEORM_ENTITIES | Path to TypeORM entity files. | ✔️ | src/infra/database/models/**/*.entity.ts |
| TYPEORM_MIGRATIONS | Path to TypeORM migration files. | ✔️ | src/infra/database/migrations/**/*.ts |
@@ -284,16 +277,7 @@ The project contains Github templates and workflows. If you don't want to keep t
## Authentication
-This boilerplate uses `Passport.js` to handle authentication. `Passport.js` is a powerful, flexible, and modular middleware that allows you to implement various authentication strategies, including social logins (e.g., Google, Facebook, GitHub, etc.).
-
-### Configuration
-
-The configuration for `Passport` is located in `src/infra/auth/authenticator/passport-authenticator.ts`. This class centralizes the setup of strategies and the implementation of required methods like `serializeUser` and `deserializeUser`.
-
-- **`serializeUser`**: Defines what data should be stored in the session. By default, it stores the user ID.
-- **`deserializeUser`**: Fetches user information based on the session data and assigns it to `req.user`. This makes the authenticated user readily accessible via `req.user` without requiring additional calls.
-
-You can find detailed documentation on `Passport.js` [here](https://www.passportjs.org/).
+This boilerplate uses JSON Web Tokens to handle authentication with `jsonwebtoken`.
### Route Protection
@@ -303,22 +287,14 @@ To ensure route security and verify the user's authentication status, this boile
This middleware ensures the user is authenticated before allowing access to the route. It integrates seamlessly with the controllers, as shown in the example below:
```typescript
-@httpPost('/logout', MIDDLEWARES_DI_TYPES.AuthenticatedMiddleware)
-public logout(): void {
- // Logout logic here
+@httpPost('/your-protected-route', MIDDLEWARES_DI_TYPES.AuthenticatedMiddleware)
+public yourProtectedRoute(): void {
+ // yourProtectedRoute logic here
}
```
This pattern allows you to secure endpoints declaratively and keeps the authentication logic consistent throughout the project.
-### Extending Authentication Strategies
-Adding new strategies is straightforward thanks to Passport's modular design. To include a new strategy:
-
-1. Install the corresponding Passport strategy package (e.g., `passport-google-oauth20`).
-2. Configure the strategy in `passport-authenticator.ts` by adding it to the existing strategies.
-
-This design simplifies the addition of new authentication methods and scales well as your application grows.
-
---
## Migrations
@@ -458,10 +434,10 @@ Use `test` to define individual tests within `describe` blocks:
```
4. **Authenticated Requests:**
-For tests requiring user authentication, create an authenticated agent:
+For tests requiring user authentication, create an authenticated request:
```typescript
- const { agent } = await testEnv.createAuthenticatedAgent();
- const res = await agent.get('/auth/authenticated');
+ const request = await testEnv.authenticatedRequest();
+ const res = await request.get('/auth/authenticated');
expect(res.body).toEqual({ authenticated: true });
```
@@ -550,7 +526,7 @@ There are 3 workflows:
2. The workflow `main-tests` is triggered when code is merged or pushed on main. It runs the tests and sends the coverage to [Codecov](https://about.codecov.io/). It has coverage for the main branch. If you don't want to keep it, you can delete the file `main-tests.yml` in the folder `workflows`.
-If you want to keep the tests on pull request but don't want to use Codecov, you can delete `main-tests` and only delete the last step `Upload coverage to Codecov` in `pull-request.yml`. You can also delete `codecov.yml`.
+If you want to keep the tests on pull request but don't want to use Codecov, you can delete `main-tests` and only delete the last step `Upload coverage to Codecov` in `pull-request.yml`.
But if you want to use CodeCov, the only thing you need to do is set your `CODECOV_TOKEN` in your github secrets.
3. The workflow `main-build` is triggered when something is merged or pulled on main. It builds the project and its primary goal is to check if main is building. If you don't want to keep it, you can delete the file `main-build.yml` in the folder `workflows`.
@@ -573,7 +549,7 @@ You can see the upcoming or in progress features [here](https://github.com/users
| --------------------------------- | --------------------------------- |
| [Express](https://expressjs.com/) | Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. |
| [TypeORM](http://typeorm.io/#/) | TypeORM is highly influenced by other ORMs, such as Hibernate, Doctrine and Entity Framework. |
-| [Passport](https://www.passportjs.org/) | Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. |
+| [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) | An implementation of JSON Web Tokens for Node.js that helps you securely transmit information between parties as a JSON object. |
| [Docker](https://www.docker.com/) | Docker is a platform designed to help developers build, share, and run modern applications. We handle the tedious setup, so you can focus on the code. |
| [PostgreSQL](https://www.postgresql.org/) | PostgreSQL is a powerful, open source object-relational database system with over 35 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance. |
| [TypeScript](https://www.typescriptlang.org/) | TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale. |
diff --git a/package.json b/package.json
index ce7933b..08ae553 100644
--- a/package.json
+++ b/package.json
@@ -27,17 +27,14 @@
},
"dependencies": {
"bcryptjs": "2.4.3",
- "connect-pg-simple": "10.0.0",
"cors": "2.8.5",
"dotenv": "16.4.7",
"express": "4.21.2",
- "express-session": "1.18.1",
"helmet": "8.0.0",
"inversify": "6.2.1",
"inversify-express-utils": "6.4.10",
+ "jsonwebtoken": "^9.0.2",
"morgan": "1.10.0",
- "passport": "0.7.0",
- "passport-local": "1.0.0",
"pg": "8.13.1",
"reflect-metadata": "0.2.2",
"tslib": "2.8.1",
@@ -53,12 +50,10 @@
"@types/connect-pg-simple": "7.0.3",
"@types/cors": "2.8.17",
"@types/express": "4.17.21",
- "@types/express-session": "1.18.1",
"@types/jest": "29.5.14",
+ "@types/jsonwebtoken": "^9.0.9",
"@types/morgan": "1.9.9",
"@types/node": "22.10.3",
- "@types/passport": "1.0.17",
- "@types/passport-local": "1.0.38",
"@types/supertest": "6.0.2",
"eslint": "9.17.0",
"eslint-import-resolver-typescript": "3.7.0",
diff --git a/repo-cover.png b/repo-cover.png
deleted file mode 100644
index af598a8..0000000
Binary files a/repo-cover.png and /dev/null differ
diff --git a/src/app/controllers/auth/auth-controller.ts b/src/app/controllers/auth/auth-controller.ts
index a24e889..92022fe 100644
--- a/src/app/controllers/auth/auth-controller.ts
+++ b/src/app/controllers/auth/auth-controller.ts
@@ -4,7 +4,6 @@ import type { NextFunction, Request, Response } from 'express';
import type { IRequestHandler } from '@/app/request-handlers/request-handler.interface';
import { REQUEST_HANDLERS_DI_TYPES } from '@/container/request-handlers/di-types';
-import { MIDDLEWARES_DI_TYPES } from '@/container/middlewares/di-types';
// The auth routes are a bit different from the other routes in the way that they don't call a use case
// but the authenticator directly because req, res and next are needed and it's not the responsibility
@@ -13,7 +12,6 @@ import { MIDDLEWARES_DI_TYPES } from '@/container/middlewares/di-types';
export class AuthController extends BaseHttpController {
constructor(
@inject(REQUEST_HANDLERS_DI_TYPES.LoginRequestHandler) private readonly loginRequestHandler: IRequestHandler,
- @inject(REQUEST_HANDLERS_DI_TYPES.LogoutRequestHandler) private readonly logoutRequestHandler: IRequestHandler,
@inject(REQUEST_HANDLERS_DI_TYPES.AuthenticatedRequestHandler) private readonly authenticatedRequestHandler: IRequestHandler,
) {
super();
@@ -28,9 +26,4 @@ export class AuthController extends BaseHttpController {
public async login(req: Request, res: Response, next: NextFunction) {
return this.loginRequestHandler.handle(req, res, next);
}
-
- @httpPost('/logout', MIDDLEWARES_DI_TYPES.AuthenticatedMiddleware)
- public async logout(req: Request, res: Response, next: NextFunction) {
- return this.logoutRequestHandler.handle(req, res, next);
- }
}
\ No newline at end of file
diff --git a/src/app/middlewares/authenticated-middleware.ts b/src/app/middlewares/authenticated-middleware.ts
index 836bce9..075ee71 100644
--- a/src/app/middlewares/authenticated-middleware.ts
+++ b/src/app/middlewares/authenticated-middleware.ts
@@ -1,21 +1,13 @@
-import { inject, injectable } from 'inversify';
+import { injectable } from 'inversify';
import { BaseMiddleware } from 'inversify-express-utils';
import type { NextFunction, Request, Response } from 'express';
-import { SERVICES_DI_TYPES } from '@/container/services/di-types';
-import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
import { HttpError } from '@/app/http-error';
@injectable()
export class AuthenticatedMiddleware extends BaseMiddleware {
- constructor(
- @inject(SERVICES_DI_TYPES.Authenticator) private readonly authenticator: IAuthenticator,
- ) {
- super();
- }
-
handler(req: Request, res: Response, next: NextFunction) {
- if (!this.authenticator.isAuthenticated(req)) {
+ if (!req.user) {
throw HttpError.forbidden('User must be authenticated');
}
diff --git a/src/app/middlewares/current-user-middleware.ts b/src/app/middlewares/current-user-middleware.ts
new file mode 100644
index 0000000..465b962
--- /dev/null
+++ b/src/app/middlewares/current-user-middleware.ts
@@ -0,0 +1,40 @@
+import type { NextFunction, Request, Response } from 'express';
+import { inject, injectable } from 'inversify';
+
+import { USE_CASES_DI_TYPES } from '@/container/use-cases/di-types';
+import type { IUseCase } from '@/core/use-case/use-case.interface';
+import type { GetCurrentUserUseCaseFailure, GetCurrentUserUseCasePayload, GetCurrentUserUseCaseSuccess } from '@/domain/use-cases/auth/get-current-user-use-case';
+
+export interface ICurrentUserMiddleware {
+ handler: (req: Request, res: Response, next: NextFunction) => Promise;
+}
+
+@injectable()
+export class CurrentUserMiddleware implements ICurrentUserMiddleware {
+ constructor(
+ @inject(USE_CASES_DI_TYPES.GetCurrentUserUseCase) private readonly getCurrentUserUseCase: IUseCase,
+ ) {}
+
+ async handler(req: Request, res: Response, next: NextFunction) {
+ try {
+ req.user = null;
+
+ const authHeader = req.headers.authorization;
+
+ if (authHeader) {
+ const [type, token] = authHeader.split(' ');
+
+ if (type === 'Bearer' && token) {
+ const result = await this.getCurrentUserUseCase.execute({ token });
+ if (result.isSuccess()) {
+ req.user = result.value.user;
+ }
+ }
+ }
+
+ next();
+ } catch (error) {
+ next(error);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/request-handlers/auth/commands/login-request-handler.ts b/src/app/request-handlers/auth/commands/login-request-handler.ts
index 8c3f044..7938405 100644
--- a/src/app/request-handlers/auth/commands/login-request-handler.ts
+++ b/src/app/request-handlers/auth/commands/login-request-handler.ts
@@ -4,13 +4,17 @@ import type { Request, Response, NextFunction } from 'express';
import type { IRequestHandler } from '@/app/request-handlers/request-handler.interface';
import { HttpError } from '@/app/http-error';
-import { SERVICES_DI_TYPES } from '@/container/services/di-types';
-import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
+import { USE_CASES_DI_TYPES } from '@/container/use-cases/di-types';
+import type { IUseCase } from '@/core/use-case/use-case.interface';
+import type { LoginUseCaseFailure, LoginUseCasePayload, LoginUseCaseSuccess } from '@/domain/use-cases/auth/login-use-case';
type ResponseBody = {
- id: string;
- username: string;
- email: string;
+ user: {
+ id: string;
+ username: string;
+ email: string;
+ };
+ token: string;
};
const payloadSchema = z.object({
@@ -18,36 +22,42 @@ const payloadSchema = z.object({
password: z.string(),
});
-// This handler, like the other auth handlers, is a bit special because it doesn't
-// execute a use case. Instead, it interacts directly with the authenticator, as
-// it requires access to req, res, and next, which are not the responsibility of the domain layer.
@injectable()
export class LoginRequestHandler implements IRequestHandler {
constructor(
- @inject(SERVICES_DI_TYPES.Authenticator) private readonly authenticator: IAuthenticator,
+ @inject(USE_CASES_DI_TYPES.LoginUseCase) private readonly loginUseCase: IUseCase,
) {}
async handle(req: Request, res: Response, next: NextFunction) {
// We don't need to get the values because the authenticator will handle the authentication
// but we validate the body to ensure the request is well-formed
- payloadSchema.parse(req.body);
-
- const { err, user } = await this.authenticator.authenticateLocal(req, res, next);
-
- if (err) {
- throw err;
- }
-
- if (!user) {
- throw HttpError.unauthorized('Incorrect credentials');
+ const { email, password } = payloadSchema.parse(req.body);
+
+ const result = await this.loginUseCase.execute({ email, password });
+
+ if (result.isSuccess()) {
+ const { user, token } = result.value;
+
+ const response: ResponseBody = {
+ user: {
+ id: user.id,
+ username: user.username,
+ email: user.email,
+ },
+ token,
+ };
+
+ res.status(200).send(response);
+ return;
+ } else if (result.isFailure()) {
+ const failure = result.failure;
+
+ switch (failure.reason) {
+ case 'InvalidCredentials':
+ throw HttpError.unauthorized('Incorrect credentials');
+ case 'UnknownError':
+ throw failure.error;
+ }
}
-
- const response: ResponseBody = {
- id: user.id,
- username: user.username,
- email: user.email,
- };
-
- res.send(response);
}
}
\ No newline at end of file
diff --git a/src/app/request-handlers/auth/commands/logout-request-handler.ts b/src/app/request-handlers/auth/commands/logout-request-handler.ts
deleted file mode 100644
index d5e349c..0000000
--- a/src/app/request-handlers/auth/commands/logout-request-handler.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { inject, injectable } from 'inversify';
-import type { Request, Response } from 'express';
-
-import type { IRequestHandler } from '@/app/request-handlers/request-handler.interface';
-import { SERVICES_DI_TYPES } from '@/container/services/di-types';
-import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
-
-type ResponseBody = {
- success: boolean;
-};
-
-// This handler, like the other auth handlers, is a bit special because it doesn't
-// execute a use case. Instead, it interacts directly with the authenticator, as
-// it requires access to req, res, and next, which are not the responsibility of the domain layer.
-@injectable()
-export class LogoutRequestHandler implements IRequestHandler {
- constructor(
- @inject(SERVICES_DI_TYPES.Authenticator) private readonly authenticator: IAuthenticator,
- ) {}
-
- async handle(req: Request, res: Response) {
- await this.authenticator.logout(req);
-
- const response: ResponseBody = {
- success: true,
- };
-
- res.send(response);
- }
-}
\ No newline at end of file
diff --git a/src/app/request-handlers/auth/queries/authenticated-request-handler.ts b/src/app/request-handlers/auth/queries/authenticated-request-handler.ts
index 24f4230..b18ff94 100644
--- a/src/app/request-handlers/auth/queries/authenticated-request-handler.ts
+++ b/src/app/request-handlers/auth/queries/authenticated-request-handler.ts
@@ -1,26 +1,19 @@
-import { inject, injectable } from 'inversify';
+import { injectable } from 'inversify';
import type { Request, Response } from 'express';
-import { SERVICES_DI_TYPES } from '@/container/services/di-types';
import type { IRequestHandler } from '@/app/request-handlers/request-handler.interface';
-import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
type ResponseBody = {
authenticated: boolean;
};
-// This handler, like the other auth handlers, is a bit special because it doesn't
-// execute a use case. Instead, it interacts directly with the authenticator, as
-// it requires access to req, res, and next, which are not the responsibility of the domain layer.
+// This handler doesn't execute a use case because it's very simple and doesn't need to
+// do anything with the domain layer.
@injectable()
export class AuthenticatedRequestHandler implements IRequestHandler {
- constructor(
- @inject(SERVICES_DI_TYPES.Authenticator) private readonly authenticator: IAuthenticator,
- ) {}
-
handle(req: Request, res: Response) {
res.send({
- authenticated: this.authenticator.isAuthenticated(req),
+ authenticated: Boolean(req.user),
});
}
}
\ No newline at end of file
diff --git a/src/app/request-handlers/users/commands/create-user-request-handler.ts b/src/app/request-handlers/users/commands/create-user-request-handler.ts
index 6606148..9ae9bf4 100644
--- a/src/app/request-handlers/users/commands/create-user-request-handler.ts
+++ b/src/app/request-handlers/users/commands/create-user-request-handler.ts
@@ -4,7 +4,7 @@ import type { Request, Response } from 'express';
import type { IRequestHandler } from '@/app/request-handlers/request-handler.interface';
import type { IUseCase } from '@/core/use-case/use-case.interface';
-import type { CreateUserCaseFailure, CreateUserCasePayload, CreateUserCaseSuccess } from '@/domain/use-cases/user/create-user-use-case';
+import type { CreateUserUseCaseFailure, CreateUserUseCasePayload, CreateUserUseCaseSuccess } from '@/domain/use-cases/user/create-user-use-case';
import { USE_CASES_DI_TYPES } from '@/container/use-cases/di-types';
import { HttpError } from '@/app/http-error';
@@ -23,7 +23,7 @@ const payloadSchema = z.object({
@injectable()
export class CreateUserRequestHandler implements IRequestHandler {
constructor(
- @inject(USE_CASES_DI_TYPES.CreateUserUseCase) private readonly createUserUseCase: IUseCase,
+ @inject(USE_CASES_DI_TYPES.CreateUserUseCase) private readonly createUserUseCase: IUseCase,
) {}
async handle(req: Request, res: Response) {
diff --git a/src/app/server.ts b/src/app/server.ts
index 1d9a16a..cd49151 100644
--- a/src/app/server.ts
+++ b/src/app/server.ts
@@ -6,12 +6,10 @@ import morgan from 'morgan';
import { json, urlencoded } from 'express';
import { env } from '@/core/env/env';
-import type { ISessionManager } from '@/domain/services/auth/session-manager.interface';
-import { SERVICES_DI_TYPES } from '@/container/services/di-types';
-import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
import { MIDDLEWARES_DI_TYPES } from '@/container/middlewares/di-types';
import { HttpError } from '@/app/http-error';
import type { IErrorMiddleware } from '@/app/middlewares/error-middleware';
+import type { ICurrentUserMiddleware } from '@/app/middlewares/current-user-middleware';
export const createServer = (container: Container) => {
const corsOptions = {
@@ -30,12 +28,8 @@ export const createServer = (container: Container) => {
app.use(helmet());
app.set('json spaces', 2);
- const session = container.get(SERVICES_DI_TYPES.SessionManager);
- app.use(session.configure());
-
- const authenticator = container.get(SERVICES_DI_TYPES.Authenticator);
- authenticator.configure();
- app.use(authenticator.session());
+ const currentUserMiddleware = container.get(MIDDLEWARES_DI_TYPES.CurrentUserMiddleware);
+ app.use(currentUserMiddleware.handler.bind(currentUserMiddleware));
});
server.setErrorConfig((app) => {
diff --git a/src/container/middlewares/container.ts b/src/container/middlewares/container.ts
index bcc8f7e..667c099 100644
--- a/src/container/middlewares/container.ts
+++ b/src/container/middlewares/container.ts
@@ -5,6 +5,8 @@ import { MIDDLEWARES_DI_TYPES } from '@/container/middlewares/di-types';
import { AuthenticatedMiddleware } from '@/app/middlewares/authenticated-middleware';
import type { IErrorMiddleware } from '@/app/middlewares/error-middleware';
import { ErrorMiddleware } from '@/app/middlewares/error-middleware';
+import type { ICurrentUserMiddleware } from '@/app/middlewares/current-user-middleware';
+import { CurrentUserMiddleware } from '@/app/middlewares/current-user-middleware';
export const registerMiddlewares = (containerBuilder: ContainerBuilder) => {
const builder = new MiddlewaresContainerBuilder(containerBuilder)
@@ -21,12 +23,21 @@ class MiddlewaresContainerBuilder {
registerMiddlewares() {
this
+ .registerCurrentUserMiddleware()
.registerAuthenticatedMiddleware()
.registerErrorMiddleware();
return this.containerBuilder;
}
+ private registerCurrentUserMiddleware() {
+ this.containerBuilder.registerActions.push((container) => {
+ container.bind(MIDDLEWARES_DI_TYPES.CurrentUserMiddleware).to(CurrentUserMiddleware).inRequestScope();
+ });
+
+ return this;
+ }
+
private registerAuthenticatedMiddleware() {
this.containerBuilder.registerActions.push((container) => {
container.bind(MIDDLEWARES_DI_TYPES.AuthenticatedMiddleware).to(AuthenticatedMiddleware).inRequestScope();
diff --git a/src/container/middlewares/di-types.ts b/src/container/middlewares/di-types.ts
index bcd7a84..0ae7939 100644
--- a/src/container/middlewares/di-types.ts
+++ b/src/container/middlewares/di-types.ts
@@ -1,4 +1,5 @@
export const MIDDLEWARES_DI_TYPES = {
AuthenticatedMiddleware: Symbol.for('AuthenticatedMiddleware'),
ErrorMiddleware: Symbol.for('ErrorMiddleware'),
+ CurrentUserMiddleware: Symbol.for('CurrentUserMiddleware'),
};
\ No newline at end of file
diff --git a/src/container/request-handlers/container.ts b/src/container/request-handlers/container.ts
index 5e35041..5f78738 100644
--- a/src/container/request-handlers/container.ts
+++ b/src/container/request-handlers/container.ts
@@ -1,5 +1,4 @@
import { LoginRequestHandler } from '@/app/request-handlers/auth/commands/login-request-handler';
-import { LogoutRequestHandler } from '@/app/request-handlers/auth/commands/logout-request-handler';
import { AuthenticatedRequestHandler } from '@/app/request-handlers/auth/queries/authenticated-request-handler';
import { HealthRequestHandler } from '@/app/request-handlers/health/queries/health-request-handler';
import type { IRequestHandler } from '@/app/request-handlers/request-handler.interface';
@@ -33,9 +32,6 @@ class RequestHandlersContainerBuilder {
this.containerBuilder.registerActions.push((container) => {
container.bind(REQUEST_HANDLERS_DI_TYPES.LoginRequestHandler).to(LoginRequestHandler).inSingletonScope();
});
- this.containerBuilder.registerActions.push((container) => {
- container.bind(REQUEST_HANDLERS_DI_TYPES.LogoutRequestHandler).to(LogoutRequestHandler).inSingletonScope();
- });
this.containerBuilder.registerActions.push((container) => {
container.bind(REQUEST_HANDLERS_DI_TYPES.AuthenticatedRequestHandler).to(AuthenticatedRequestHandler).inSingletonScope();
});
diff --git a/src/container/request-handlers/di-types.ts b/src/container/request-handlers/di-types.ts
index 88cd974..44a1ad4 100644
--- a/src/container/request-handlers/di-types.ts
+++ b/src/container/request-handlers/di-types.ts
@@ -1,6 +1,5 @@
export const REQUEST_HANDLERS_DI_TYPES = {
LoginRequestHandler: Symbol.for('LoginRequestHandler'),
- LogoutRequestHandler: Symbol.for('LogoutRequestHandler'),
AuthenticatedRequestHandler: Symbol.for('AuthenticatedRequestHandler'),
CreateUserRequestHandler: Symbol.for('CreateUserRequestHandler'),
HealthRequestHandler: Symbol.for('HealthRequestHandler'),
diff --git a/src/container/services/container.ts b/src/container/services/container.ts
index 4a2da08..5e98ba2 100644
--- a/src/container/services/container.ts
+++ b/src/container/services/container.ts
@@ -1,12 +1,11 @@
import type { BuildContainerOptions, ContainerBuilder } from '@/container/container';
+import { REPOSITORIES_DI_TYPES } from '@/container/repositories/di-types';
import { SERVICES_DI_TYPES } from '@/container/services/di-types';
-import { integerEnv, mandatoryEnv, mandatoryIntegerEnv, booleanEnv, unionEnv, env } from '@/core/env/env';
+import { integerEnv, mandatoryEnv, mandatoryIntegerEnv, booleanEnv, env } from '@/core/env/env';
+import type { IUserRepository } from '@/domain/repositories/user-repository.interface';
import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
-import type { ISessionManager } from '@/domain/services/auth/session-manager.interface';
import type { IEncryptor } from '@/domain/services/security/encryptor.interface';
-import { PassportAuthenticator } from '@/infra/auth/authenticator/passport-authenticator';
-import type { SessionConfig } from '@/infra/auth/session/express-session-manager';
-import { ExpressSessionManager } from '@/infra/auth/session/express-session-manager';
+import { JwtAuthenticator } from '@/infra/auth/authenticator/jwt-authenticator';
import type { IDatabase, DatabaseConfig } from '@/infra/database/database';
import { Database } from '@/infra/database/database';
import { BcryptEncryptor } from '@/infra/security/encryptor/bcrypt-encryptor';
@@ -46,13 +45,15 @@ class ServicesContainerBuilder {
}
private registerAuthServices() {
- const config = this.getSessionManagerConfig();
- this.containerBuilder.registerActions.push((container) => {
- container.bind(SERVICES_DI_TYPES.SessionManager).toDynamicValue(() => new ExpressSessionManager(config)).inSingletonScope();
- });
-
+ const config = {
+ secret: mandatoryEnv('JWT_SECRET'),
+ expiresInSeconds: integerEnv('JWT_EXPIRES_IN_SECONDS', 86400), // 1 day in seconds (default)
+ };
this.containerBuilder.registerActions.push((container) => {
- container.bind(SERVICES_DI_TYPES.Authenticator).to(PassportAuthenticator).inSingletonScope();
+ container.bind(SERVICES_DI_TYPES.Authenticator).toDynamicValue(() => new JwtAuthenticator(
+ config,
+ container.get(REPOSITORIES_DI_TYPES.UserRepository),
+ )).inSingletonScope();
});
return this;
@@ -77,29 +78,6 @@ class ServicesContainerBuilder {
return this;
}
- private getSessionManagerConfig(): SessionConfig {
- const nodeEnv = mandatoryEnv('NODE_ENV');
-
- return {
- storeConfig: nodeEnv === 'test' ? undefined : {
- host: mandatoryEnv('DB_HOST'),
- port: mandatoryIntegerEnv('DB_PORT'),
- user: mandatoryEnv('DB_USER'),
- password: mandatoryEnv('DB_PASSWORD'),
- database: mandatoryEnv('DB_NAME'),
- },
- secret: env('SESSION_SECRET', 'session-secret'),
- resave: booleanEnv('SESSION_RESAVE', false),
- saveUninitialized: booleanEnv('SESSION_SAVE_UNINITIALIZED', false),
- cookie: {
- secure: booleanEnv('SESSION_COOKIE_SECURE', false),
- maxAge: integerEnv('SESSION_COOKIE_MAX_AGE', 90 * 24 * 60 * 60 * 1000), // 90 days by default
- httpOnly: booleanEnv('SESSION_COOKIE_HTTP_ONLY', false),
- sameSite: unionEnv('SESSION_COOKIE_SAME_SITE', ['strict', 'lax', 'none',], 'lax'),
- },
- };
- }
-
private getDatabaseConfig(): DatabaseConfig {
const isTest = mandatoryEnv('NODE_ENV') === 'test';
diff --git a/src/container/services/di-types.ts b/src/container/services/di-types.ts
index 7f193d7..deede08 100644
--- a/src/container/services/di-types.ts
+++ b/src/container/services/di-types.ts
@@ -1,6 +1,5 @@
export const SERVICES_DI_TYPES = {
Database: Symbol.for('Database'),
Encryptor: Symbol.for('Encryptor'),
- SessionManager: Symbol.for('SessionManager'),
Authenticator: Symbol.for('Authenticator'),
};
\ No newline at end of file
diff --git a/src/container/use-cases/container.ts b/src/container/use-cases/container.ts
index a55c9cf..64bf7d8 100644
--- a/src/container/use-cases/container.ts
+++ b/src/container/use-cases/container.ts
@@ -1,6 +1,8 @@
import type { ContainerBuilder } from '@/container/container';
import { USE_CASES_DI_TYPES } from '@/container/use-cases/di-types';
import type { IUseCase } from '@/core/use-case/use-case.interface';
+import { GetCurrentUserUseCase } from '@/domain/use-cases/auth/get-current-user-use-case';
+import { LoginUseCase } from '@/domain/use-cases/auth/login-use-case';
import { CreateUserUseCase } from '@/domain/use-cases/user/create-user-use-case';
export const registerUseCases = (containerBuilder: ContainerBuilder) => {
@@ -17,12 +19,26 @@ class UseCasesContainerBuilder {
constructor(private readonly containerBuilder: ContainerBuilder) {}
registerUseCases() {
- this.registerCreateUserUseCase();
+ this
+ .registerAuthUseCases()
+ .registerUserUseCases();
return this.containerBuilder;
}
- private registerCreateUserUseCase() {
+ private registerAuthUseCases() {
+ this.containerBuilder.registerActions.push((container) => {
+ container.bind(USE_CASES_DI_TYPES.LoginUseCase).to(LoginUseCase).inSingletonScope();
+ });
+
+ this.containerBuilder.registerActions.push((container) => {
+ container.bind(USE_CASES_DI_TYPES.GetCurrentUserUseCase).to(GetCurrentUserUseCase).inSingletonScope();
+ });
+
+ return this;
+ }
+
+ private registerUserUseCases() {
this.containerBuilder.registerActions.push((container) => {
container.bind(USE_CASES_DI_TYPES.CreateUserUseCase).to(CreateUserUseCase).inSingletonScope();
});
diff --git a/src/container/use-cases/di-types.ts b/src/container/use-cases/di-types.ts
index 2c0f36f..76e89f8 100644
--- a/src/container/use-cases/di-types.ts
+++ b/src/container/use-cases/di-types.ts
@@ -1,3 +1,5 @@
export const USE_CASES_DI_TYPES = {
CreateUserUseCase: Symbol.for('CreateUserUseCase'),
+ LoginUseCase: Symbol.for('LoginUseCase'),
+ GetCurrentUserUseCase: Symbol.for('GetCurrentUserUseCase'),
};
\ No newline at end of file
diff --git a/src/domain/services/auth/authenticator.interface.ts b/src/domain/services/auth/authenticator.interface.ts
index b222ed6..23226ff 100644
--- a/src/domain/services/auth/authenticator.interface.ts
+++ b/src/domain/services/auth/authenticator.interface.ts
@@ -1,15 +1,22 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
import type { User } from '@/domain/models/user';
export type AuthenticateResponse = {
- err?: any;
- user?: User | false | null;
+ success: true;
+ user: User;
+ token: string;
+} | {
+ success: false;
+};
+
+export type GetAuthenticatedUserResponse = {
+ success: true;
+ user: User;
+} | {
+ success: false;
+ reason: 'InvalidToken' | 'UserNotFound';
};
export interface IAuthenticator {
- configure: () => void;
- session: () => any;
- authenticateLocal: (req: any, res: any, next: any) => Promise;
- isAuthenticated: (req: any) => boolean;
- logout: (req: any) => Promise;
+ authenticate: (email: string, password: string) => Promise;
+ getAuthenticatedUser: (token: string) => Promise;
}
\ No newline at end of file
diff --git a/src/domain/services/auth/session-manager.interface.ts b/src/domain/services/auth/session-manager.interface.ts
deleted file mode 100644
index 8b35341..0000000
--- a/src/domain/services/auth/session-manager.interface.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-export type ISessionManager = {
- configure(): any;
-};
\ No newline at end of file
diff --git a/src/domain/use-cases/auth/get-current-user-use-case.ts b/src/domain/use-cases/auth/get-current-user-use-case.ts
new file mode 100644
index 0000000..125e24f
--- /dev/null
+++ b/src/domain/use-cases/auth/get-current-user-use-case.ts
@@ -0,0 +1,47 @@
+import { inject, injectable } from 'inversify';
+
+import type { IUseCase } from '@/core/use-case/use-case.interface';
+import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
+import { SERVICES_DI_TYPES } from '@/container/services/di-types';
+import type { User } from '@/domain/models/user';
+import { Failure, Success } from '@/core/result/result';
+
+export type GetCurrentUserUseCasePayload = {
+ token: string;
+};
+
+type GetCurrentUserUseCaseFailureReason = 'InvalidToken' | 'UserNotFound' | 'UnknownError';
+export type GetCurrentUserUseCaseFailure = {
+ reason: GetCurrentUserUseCaseFailureReason;
+ error: Error;
+};
+
+export type GetCurrentUserUseCaseSuccess = {
+ user: User;
+};
+
+@injectable()
+export class GetCurrentUserUseCase implements IUseCase {
+ constructor(
+ @inject(SERVICES_DI_TYPES.Authenticator) private readonly authenticator: IAuthenticator,
+ ) {}
+
+ async execute(payload: GetCurrentUserUseCasePayload) {
+ try {
+ const result = await this.authenticator.getAuthenticatedUser(payload.token);
+ if (result.success) {
+ return new Success({ user: result.user });
+ }
+
+ return new Failure({
+ reason: result.reason,
+ error: new Error(`Failed to get current user: ${result.reason}`),
+ });
+ } catch (error) {
+ return new Failure({
+ reason: 'UnknownError',
+ error: error as Error,
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/domain/use-cases/auth/login-use-case.ts b/src/domain/use-cases/auth/login-use-case.ts
new file mode 100644
index 0000000..4db3052
--- /dev/null
+++ b/src/domain/use-cases/auth/login-use-case.ts
@@ -0,0 +1,51 @@
+import { inject, injectable } from 'inversify';
+
+import type { IUseCase } from '@/core/use-case/use-case.interface';
+import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
+import { SERVICES_DI_TYPES } from '@/container/services/di-types';
+import type { User } from '@/domain/models/user';
+import { Failure, Success } from '@/core/result/result';
+
+export type LoginUseCasePayload = {
+ email: string;
+ password: string;
+};
+
+type LoginUseCaseFailureReason = 'InvalidCredentials' | 'UnknownError';
+export type LoginUseCaseFailure = {
+ reason: LoginUseCaseFailureReason;
+ error: Error;
+};
+
+export type LoginUseCaseSuccess = {
+ user: User;
+ token: string;
+};
+
+@injectable()
+export class LoginUseCase implements IUseCase {
+ constructor(
+ @inject(SERVICES_DI_TYPES.Authenticator) private readonly authenticator: IAuthenticator,
+ ) {}
+
+ async execute(payload: LoginUseCasePayload) {
+ try {
+ const result = await this.authenticator.authenticate(payload.email, payload.password);
+ if (!result.success) {
+ return new Failure({
+ reason: 'InvalidCredentials',
+ error: new Error('Invalid credentials'),
+ });
+ }
+
+ const { user, token } = result;
+
+ return new Success({ user, token });
+ } catch (error) {
+ return new Failure({
+ reason: 'UnknownError',
+ error: error as Error,
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/domain/use-cases/user/create-user-use-case.ts b/src/domain/use-cases/user/create-user-use-case.ts
index 3a503ac..1e86eed 100644
--- a/src/domain/use-cases/user/create-user-use-case.ts
+++ b/src/domain/use-cases/user/create-user-use-case.ts
@@ -11,24 +11,24 @@ import { User } from '@/domain/models/user';
import type { IUserRepository } from '@/domain/repositories/user-repository.interface';
import type { IEncryptor } from '@/domain/services/security/encryptor.interface';
-export type CreateUserCasePayload = {
+export type CreateUserUseCasePayload = {
email: string;
username: string;
password: string;
};
-type CreateUserCaseFailureReason = 'EmailAlreadyExists' | 'UsernameAlreadyExists' | 'UnknownError';
-export type CreateUserCaseFailure = {
- reason: CreateUserCaseFailureReason;
+type CreateUserUseCaseFailureReason = 'EmailAlreadyExists' | 'UsernameAlreadyExists' | 'UnknownError';
+export type CreateUserUseCaseFailure = {
+ reason: CreateUserUseCaseFailureReason;
error: Error;
};
-export type CreateUserCaseSuccess = {
+export type CreateUserUseCaseSuccess = {
user: User;
};
@injectable()
-export class CreateUserUseCase implements IUseCase {
+export class CreateUserUseCase implements IUseCase {
constructor(
@inject(CORE_DI_TYPES.IDGenerator) private readonly idGenerator: IIDGenerator,
@inject(CORE_DI_TYPES.Time) private readonly time: ITime,
@@ -36,11 +36,11 @@ export class CreateUserUseCase implements IUseCase({
+ return new Failure({
reason: 'EmailAlreadyExists',
error: new Error('Email already exists'),
});
@@ -48,7 +48,7 @@ export class CreateUserUseCase implements IUseCase({
+ return new Failure({
reason: 'UsernameAlreadyExists',
error: new Error('Username already exists'),
});
@@ -69,7 +69,7 @@ export class CreateUserUseCase implements IUseCase({
+ return new Failure({
reason: 'UnknownError',
error: error as Error,
});
diff --git a/src/infra/auth/authenticator/jwt-authenticator.ts b/src/infra/auth/authenticator/jwt-authenticator.ts
new file mode 100644
index 0000000..1aaa804
--- /dev/null
+++ b/src/infra/auth/authenticator/jwt-authenticator.ts
@@ -0,0 +1,59 @@
+import { inject, injectable } from 'inversify';
+import jwt from 'jsonwebtoken';
+
+import { REPOSITORIES_DI_TYPES } from '@/container/repositories/di-types';
+import type { IUserRepository } from '@/domain/repositories/user-repository.interface';
+import type { IAuthenticator, AuthenticateResponse, GetAuthenticatedUserResponse } from '@/domain/services/auth/authenticator.interface';
+
+type JwtPayload = {
+ id: string;
+};
+
+@injectable()
+export class JwtAuthenticator implements IAuthenticator {
+ constructor(
+ private readonly config: {
+ secret: string;
+ expiresInSeconds: number;
+ },
+ @inject(REPOSITORIES_DI_TYPES.UserRepository) private readonly userRepository: IUserRepository,
+ ) {}
+
+ async authenticate(email: string, password: string): Promise {
+ const user = await this.userRepository.findOneByEmailPassword(email, password);
+ if (!user) {
+ return { success: false };
+ }
+
+ const token = jwt.sign({ id: user.id }, this.config.secret, {
+ expiresIn: this.config.expiresInSeconds,
+ });
+
+ return { success: true, user, token };
+ }
+
+ async getAuthenticatedUser(token: string): Promise {
+ let decoded: JwtPayload;
+ try {
+ decoded = jwt.verify(token, this.config.secret) as JwtPayload;
+ } catch {
+ return {
+ success: false,
+ reason: 'InvalidToken',
+ };
+ }
+
+ const user = await this.userRepository.findOneById(decoded.id);
+ if (!user) {
+ return {
+ success: false,
+ reason: 'UserNotFound',
+ };
+ }
+
+ return {
+ success: true,
+ user,
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/infra/auth/authenticator/passport-authenticator.ts b/src/infra/auth/authenticator/passport-authenticator.ts
deleted file mode 100644
index 4d88241..0000000
--- a/src/infra/auth/authenticator/passport-authenticator.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { inject, injectable } from 'inversify';
-import passport from 'passport';
-import { Strategy as LocalStrategy } from 'passport-local';
-import type { NextFunction, Request, Response } from 'express';
-
-import { REPOSITORIES_DI_TYPES } from '@/container/repositories/di-types';
-import { RequestUser } from '@/domain/models/request-user';
-import type { User } from '@/domain/models/user';
-import type { IUserRepository } from '@/domain/repositories/user-repository.interface';
-import type { IAuthenticator, AuthenticateResponse } from '@/domain/services/auth/authenticator.interface';
-
-@injectable()
-export class PassportAuthenticator implements IAuthenticator {
- constructor(
- @inject(REPOSITORIES_DI_TYPES.UserRepository) private readonly userRepository: IUserRepository,
- ) {}
-
- configure() {
- passport.use(this.getLocalStrategy());
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- passport.serializeUser((user: any, done) => {
- done(null, user.id);
- });
-
- passport.deserializeUser(async (req, id, done: (err: unknown, user: RequestUser | null) => void) => {
- const user = await this.userRepository.findOneById(id);
-
- if (!user) {
- // if passport tries to deserialize user but id doesn't exist anymore in db,
- // it means the user has been deleted, so logout the request
- req.logout(() => undefined);
- done(null, null);
- } else {
- done(null, RequestUser.fromUser(user));
- }
- });
- }
-
- session() {
- return passport.session();
- }
-
- isAuthenticated(req: Request): boolean {
- return req.isAuthenticated();
- }
-
- authenticateLocal(req: Request, res: Response, next: NextFunction): Promise {
- return new Promise((resolve) => {
- passport.authenticate('local', (err?: unknown, user?: User | false | null) => {
- if (err) {
- return resolve({ err });
- }
-
- if (!user) {
- return resolve({ user });
- }
-
- req.logIn(user, (loginErr) => {
- if (loginErr) {
- return resolve({ err: loginErr });
- }
- return resolve({ user });
- });
-
- return undefined;
- })(req, res, next);
- });
- }
-
- async logout(req: Request): Promise {
- await new Promise((resolve, reject) => {
- req.logout((err) => {
- if (err) {
- return reject(err);
- }
- req.session.destroy(() => resolve());
- return undefined;
- });
- });
- }
-
- private getLocalStrategy() {
- return new LocalStrategy(
- {
- usernameField: 'email',
- passwordField: 'password',
- },
- async (email, password, done: (err: unknown, user?: User) => void) => {
- try {
- // Search a user whose username or email is the login parameter
- const user = await this.userRepository.findOneByEmailPassword(email, password);
-
- // If the user doesn't exist or the password is wrong, return error as null and user as undefined
- // It allows to distinguish technical error and wrong credentials
- if (!user) {
- return done(null, undefined);
- }
-
- return done(null, user);
- } catch (err) {
- return done(err);
- }
- },
- );
- }
-}
\ No newline at end of file
diff --git a/src/infra/auth/session/express-session-manager.ts b/src/infra/auth/session/express-session-manager.ts
deleted file mode 100644
index bc97128..0000000
--- a/src/infra/auth/session/express-session-manager.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import pgSession from 'connect-pg-simple';
-import session, { type CookieOptions, type SessionOptions } from 'express-session';
-import { injectable } from 'inversify';
-
-import type { ISessionManager } from '@/domain/services/auth/session-manager.interface';
-
-type SessionStoreConfig = {
- host: string;
- port: number;
- user: string;
- password: string;
- database: string;
-};
-
-export type SessionConfig = {
- storeConfig?: SessionStoreConfig;
- secret: SessionOptions['secret'];
- resave: SessionOptions['resave'];
- saveUninitialized: SessionOptions['saveUninitialized'];
- cookie: {
- secure: CookieOptions['secure'];
- maxAge: CookieOptions['maxAge'];
- httpOnly: CookieOptions['httpOnly'];
- sameSite: 'strict' | 'lax' | 'none';
- };
-};
-
-@injectable()
-export class ExpressSessionManager implements ISessionManager {
- constructor(private readonly config: SessionConfig) {}
-
- configure() {
- let sessionStore: pgSession.PGStore | undefined;
- if (this.config.storeConfig) {
- const postgresSession = pgSession(session);
- sessionStore = new postgresSession({
- conObject: this.config.storeConfig,
- createTableIfMissing: true,
- });
- }
-
- return session({
- store: sessionStore,
- secret: this.config.secret,
- resave: this.config.resave,
- saveUninitialized: this.config.saveUninitialized,
- cookie: {
- secure: this.config.cookie.secure,
- maxAge: this.config.cookie.maxAge,
- httpOnly: this.config.cookie.httpOnly,
- sameSite: this.config.cookie.sameSite,
- },
- });
- }
-}
\ No newline at end of file
diff --git a/src/tests/e2e/auth/authenticated.test.ts b/src/tests/e2e/auth/authenticated.test.ts
index 03e3570..69b8980 100644
--- a/src/tests/e2e/auth/authenticated.test.ts
+++ b/src/tests/e2e/auth/authenticated.test.ts
@@ -17,9 +17,9 @@ describe('GET auth/authenticated', () => {
});
test('Return true when user is authenticated', async () => {
- const { agent } = await testEnv.createAuthenticatedAgent();
+ const request = await testEnv.authenticatedRequest();
- const res = await agent.get('/auth/authenticated');
+ const res = await request.get('/auth/authenticated');
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual({
diff --git a/src/tests/e2e/auth/login.test.ts b/src/tests/e2e/auth/login.test.ts
index 5708c8f..eca00b9 100644
--- a/src/tests/e2e/auth/login.test.ts
+++ b/src/tests/e2e/auth/login.test.ts
@@ -30,9 +30,12 @@ describe('POST auth/login', () => {
expect(res.statusCode).toEqual(200);
expect(res.body).toEqual({
- id: expect.any(String),
- username,
- email,
+ user: {
+ id: expect.any(String),
+ username,
+ email,
+ },
+ token: expect.any(String),
});
});
});
\ No newline at end of file
diff --git a/src/tests/e2e/auth/logout.test.ts b/src/tests/e2e/auth/logout.test.ts
deleted file mode 100644
index fc27839..0000000
--- a/src/tests/e2e/auth/logout.test.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { createTestEnvironment } from '@/tests/helpers/test-helpers';
-import type { TestEnvironment } from '@/tests/helpers/test-environment';
-
-let testEnv: TestEnvironment;
-
-beforeAll(async () => {
- testEnv = await createTestEnvironment();
-});
-
-afterAll(async () => {
- await testEnv.close();
-});
-
-describe('POST auth/logout', () => {
- beforeEach(async () => {
- await testEnv.clearDatabase();
- });
-
- test('Logout a user', async () => {
- const { agent } = await testEnv.createAuthenticatedAgent();
-
- const res = await agent.post('/auth/logout');
-
- expect(res.statusCode).toEqual(200);
- expect(res.body).toEqual({
- success: true,
- });
- });
-});
\ No newline at end of file
diff --git a/src/tests/helpers/test-environment.ts b/src/tests/helpers/test-environment.ts
index b1951af..5008a4f 100644
--- a/src/tests/helpers/test-environment.ts
+++ b/src/tests/helpers/test-environment.ts
@@ -36,25 +36,20 @@ export class TestEnvironment {
}
/**
- * Create a test agent.
- * @returns The created agent.
+ * Create a test client.
+ * @returns The created client.
*/
request() {
return request(this.server);
}
- /**
- * Create an authenticated test agent. A test agent allows to maintain session between multiple requests.
- * @param testUserOptions - The authenticated user informations. Optional.
- * @returns The created agent.
- */
- async createAuthenticatedAgent(testUserOptions?: CreateTestUserOptions) {
- const userAgent = request.agent(this.server);
+ async authenticatedRequest(testUserOptions?: CreateTestUserOptions) {
const user = await createTestUser(this, testUserOptions);
- await userAgent
+ const agent = request.agent(this.server);
+ const res = await agent
.post('/auth/login')
.send({ email: user.email, password: testUserOptions?.password || 'password' });
- return { agent: userAgent, user };
+ return agent.set({ Authorization: `Bearer ${res.body.token}` });
}
-}
\ No newline at end of file
+}
diff --git a/src/tests/unit/app/middlewares/authenticated-middleware.test.ts b/src/tests/unit/app/middlewares/authenticated-middleware.test.ts
index e1709af..a05e4e5 100644
--- a/src/tests/unit/app/middlewares/authenticated-middleware.test.ts
+++ b/src/tests/unit/app/middlewares/authenticated-middleware.test.ts
@@ -1,34 +1,30 @@
-import { mock } from 'jest-mock-extended';
import type { Request, Response } from 'express';
-import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
import { AuthenticatedMiddleware } from '@/app/middlewares/authenticated-middleware';
import { HttpError } from '@/app/http-error';
describe('AuthenticatedMiddleware', () => {
test('Call next if user is authenticated', () => {
- const authenticatorMock = mock();
- authenticatorMock.isAuthenticated.mockReturnValue(true);
-
- const req = {} as Request;
+ const req = {
+ user: {
+ id: '1',
+ },
+ } as Request;
const res = {} as Response;
const next = jest.fn();
- const middleware = new AuthenticatedMiddleware(authenticatorMock);
+ const middleware = new AuthenticatedMiddleware();
middleware.handler(req, res, next);
expect(next).toHaveBeenCalled();
});
test('Throw an error when user is not authenticated', () => {
- const authenticatorMock = mock();
- authenticatorMock.isAuthenticated.mockReturnValue(false);
-
const req = {} as Request;
const res = {} as Response;
const next = jest.fn();
- const middleware = new AuthenticatedMiddleware(authenticatorMock);
+ const middleware = new AuthenticatedMiddleware();
expect(() => middleware.handler(req, res, next)).toThrow(HttpError.forbidden('User must be authenticated'));
});
});
diff --git a/src/tests/unit/app/middlewares/current-user-middleware.test.ts b/src/tests/unit/app/middlewares/current-user-middleware.test.ts
new file mode 100644
index 0000000..4e8c99e
--- /dev/null
+++ b/src/tests/unit/app/middlewares/current-user-middleware.test.ts
@@ -0,0 +1,119 @@
+import { mock } from 'jest-mock-extended';
+import type { Request, Response } from 'express';
+
+import { CurrentUserMiddleware } from '@/app/middlewares/current-user-middleware';
+import type { IUseCase } from '@/core/use-case/use-case.interface';
+import type { GetCurrentUserUseCaseFailure, GetCurrentUserUseCasePayload, GetCurrentUserUseCaseSuccess } from '@/domain/use-cases/auth/get-current-user-use-case';
+import { Failure, Success } from '@/core/result/result';
+import { User } from '@/domain/models/user';
+
+describe('CurrentUserMiddleware', () => {
+ test('Set user in request if token is valid', async () => {
+ const getCurrentUserUseCase = mock>();
+
+ const req = {
+ headers: {
+ authorization: 'Bearer token',
+ },
+ } as Request;
+ const res = mock();
+ const next = jest.fn();
+
+ const user = new User({
+ id: '1',
+ username: 'test',
+ email: 'test@test.com',
+ hashPassword: 'test',
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ });
+
+ getCurrentUserUseCase.execute.mockResolvedValue(new Success({ user }));
+
+ const middleware = new CurrentUserMiddleware(getCurrentUserUseCase);
+ await middleware.handler(req, res, next);
+
+ expect(getCurrentUserUseCase.execute).toHaveBeenCalledWith({ token: 'token' });
+ expect(req.user).toEqual(user);
+ });
+
+ test('Set user to null in request if token is invalid', async () => {
+ const getCurrentUserUseCase = mock>();
+
+ const req = {
+ headers: {
+ authorization: 'Bearer invalid-token',
+ },
+ } as Request;
+ const res = mock();
+ const next = jest.fn();
+
+ getCurrentUserUseCase.execute.mockResolvedValue(new Failure({
+ reason: 'InvalidToken',
+ error: new Error('Failed to get current user: InvalidToken'),
+ }));
+
+ const middleware = new CurrentUserMiddleware(getCurrentUserUseCase);
+ await middleware.handler(req, res, next);
+
+ expect(getCurrentUserUseCase.execute).toHaveBeenCalledWith({ token: 'invalid-token' });
+ expect(req.user).toBeNull();
+ });
+
+ test('Set user to null in request if user is not found', async () => {
+ const getCurrentUserUseCase = mock>();
+
+ const req = {
+ headers: {
+ authorization: 'Bearer token',
+ },
+ } as Request;
+ const res = mock();
+ const next = jest.fn();
+
+ getCurrentUserUseCase.execute.mockResolvedValue(new Failure({
+ reason: 'UserNotFound',
+ error: new Error('Failed to get current user: UserNotFound'),
+ }));
+
+ const middleware = new CurrentUserMiddleware(getCurrentUserUseCase);
+ await middleware.handler(req, res, next);
+
+ expect(getCurrentUserUseCase.execute).toHaveBeenCalledWith({ token: 'token' });
+ expect(req.user).toBeNull();
+ });
+
+ test("Don't call use case and set user to null if token is not provided", async () => {
+ const getCurrentUserUseCase = mock>();
+
+ const req = {
+ headers: {},
+ } as Request;
+ const res = mock();
+ const next = jest.fn();
+
+ const middleware = new CurrentUserMiddleware(getCurrentUserUseCase);
+ await middleware.handler(req, res, next);
+
+ expect(getCurrentUserUseCase.execute).not.toHaveBeenCalled();
+ expect(req.user).toBeNull();
+ });
+
+ test("Don't call use case and set user to null if token is not Bearer", async () => {
+ const getCurrentUserUseCase = mock>();
+
+ const req = {
+ headers: {
+ authorization: 'Basic token',
+ },
+ } as Request;
+ const res = mock();
+ const next = jest.fn();
+
+ const middleware = new CurrentUserMiddleware(getCurrentUserUseCase);
+ await middleware.handler(req, res, next);
+
+ expect(getCurrentUserUseCase.execute).not.toHaveBeenCalled();
+ expect(req.user).toBeNull();
+ });
+});
\ No newline at end of file
diff --git a/src/tests/unit/app/request-handlers/auth/commands/login-request-handler.test.ts b/src/tests/unit/app/request-handlers/auth/commands/login-request-handler.test.ts
index de80998..affc9ee 100644
--- a/src/tests/unit/app/request-handlers/auth/commands/login-request-handler.test.ts
+++ b/src/tests/unit/app/request-handlers/auth/commands/login-request-handler.test.ts
@@ -2,12 +2,13 @@ import { mock } from 'jest-mock-extended';
import type { Request, Response } from 'express';
import { HttpError } from '@/app/http-error';
-import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
import { User } from '@/domain/models/user';
import { LoginRequestHandler } from '@/app/request-handlers/auth/commands/login-request-handler';
+import type { IUseCase } from '@/core/use-case/use-case.interface';
+import { Failure, Success } from '@/core/result/result';
describe('LoginRequestHandler', () => {
- test('Send user infos after login', async () => {
+ test('Send user infos and token after login', async () => {
const req = {
body: {
email: 'test@test.com',
@@ -15,28 +16,34 @@ describe('LoginRequestHandler', () => {
},
} as Request;
const res = mock();
+ res.status.mockReturnThis();
const next = jest.fn();
- const authenticator = mock();
- authenticator.authenticateLocal.mockResolvedValue({
- err: null,
- user: new User({
- id: '1',
- username: 'test_username',
- email: 'test@test.com',
- createdAt: new Date(),
- updatedAt: new Date(),
- hashPassword: 'test_hashpassword',
- }),
- });
+ const loginUseCase = mock();
+ loginUseCase.execute.mockResolvedValue(
+ new Success({
+ user: new User({
+ id: '1',
+ username: 'test_username',
+ email: 'test@test.com',
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ hashPassword: 'test_hashpassword',
+ }),
+ token: 'test_token',
+ })
+ );
- const handler = new LoginRequestHandler(authenticator);
+ const handler = new LoginRequestHandler(loginUseCase);
await handler.handle(req, res, next);
expect(res.send).toHaveBeenCalledWith({
- id: '1',
- username: 'test_username',
- email: 'test@test.com',
+ user: {
+ id: '1',
+ username: 'test_username',
+ email: 'test@test.com',
+ },
+ token: 'test_token',
});
});
@@ -48,15 +55,18 @@ describe('LoginRequestHandler', () => {
},
} as Request;
const res = mock();
+ res.status.mockReturnThis();
const next = jest.fn();
- const authenticator = mock();
- authenticator.authenticateLocal.mockResolvedValue({
- err: null,
- user: null,
- });
+ const loginUseCase = mock();
+ loginUseCase.execute.mockResolvedValue(
+ new Failure({
+ reason: 'InvalidCredentials',
+ error: new Error('Incorrect credentials'),
+ }),
+ );
- const handler = new LoginRequestHandler(authenticator);
+ const handler = new LoginRequestHandler(loginUseCase);
await expect(handler.handle(req, res, next)).rejects.toStrictEqual(HttpError.unauthorized('Incorrect credentials'));
});
});
diff --git a/src/tests/unit/app/request-handlers/auth/commands/logout-request-handler.test.ts b/src/tests/unit/app/request-handlers/auth/commands/logout-request-handler.test.ts
deleted file mode 100644
index e4605fc..0000000
--- a/src/tests/unit/app/request-handlers/auth/commands/logout-request-handler.test.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { mock } from 'jest-mock-extended';
-import type { Request, Response } from 'express';
-
-import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
-import { LogoutRequestHandler } from '@/app/request-handlers/auth/commands/logout-request-handler';
-
-describe('LogoutRequestHandler', () => {
- test('Call authenticator logout and send success response', async () => {
- const req = {} as Request;
- const res = mock();
-
- const authenticator = mock();
-
- const handler = new LogoutRequestHandler(authenticator);
- await handler.handle(req, res);
-
- expect(authenticator.logout).toHaveBeenCalledWith(req);
- expect(res.send).toHaveBeenCalledWith({
- success: true,
- });
- });
-});
diff --git a/src/tests/unit/app/request-handlers/auth/queries/authenticated-request-handler.test.ts b/src/tests/unit/app/request-handlers/auth/queries/authenticated-request-handler.test.ts
index 67b622e..0acf6d7 100644
--- a/src/tests/unit/app/request-handlers/auth/queries/authenticated-request-handler.test.ts
+++ b/src/tests/unit/app/request-handlers/auth/queries/authenticated-request-handler.test.ts
@@ -2,17 +2,17 @@ import { mock } from 'jest-mock-extended';
import type { Request, Response } from 'express';
import { AuthenticatedRequestHandler } from '@/app/request-handlers/auth/queries/authenticated-request-handler';
-import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
describe('AuthenticatedRequestHandler', () => {
test('Send authenticated true', () => {
- const req = {} as Request;
+ const req = {
+ user: {
+ id: '1',
+ },
+ } as Request;
const res = mock();
- const authenticator = mock();
- authenticator.isAuthenticated.mockReturnValue(true);
-
- const handler = new AuthenticatedRequestHandler(authenticator);
+ const handler = new AuthenticatedRequestHandler();
handler.handle(req, res);
expect(res.send).toHaveBeenCalledWith({
@@ -24,10 +24,7 @@ describe('AuthenticatedRequestHandler', () => {
const req = {} as Request;
const res = mock();
- const authenticator = mock();
- authenticator.isAuthenticated.mockReturnValue(false);
-
- const handler = new AuthenticatedRequestHandler(authenticator);
+ const handler = new AuthenticatedRequestHandler();
handler.handle(req, res);
expect(res.send).toHaveBeenCalledWith({
diff --git a/src/tests/unit/app/request-handlers/users/commands/create-user-request-handler.test.ts b/src/tests/unit/app/request-handlers/users/commands/create-user-request-handler.test.ts
index 3f76944..b7870ba 100644
--- a/src/tests/unit/app/request-handlers/users/commands/create-user-request-handler.test.ts
+++ b/src/tests/unit/app/request-handlers/users/commands/create-user-request-handler.test.ts
@@ -9,7 +9,7 @@ import { HttpError } from '@/app/http-error';
import { CreateUserRequestHandler } from '@/app/request-handlers/users/commands/create-user-request-handler';
describe('CreateUserRequestHandler', () => {
- test('Call CreateUserUserCase and send success response with user infos', async () => {
+ test('Call CreateUserUseCase and send success response with user infos', async () => {
const req = {
body: {
username: 'test_username',
diff --git a/src/tests/unit/domain/use-cases/auth/get-current-user-use-case.test.ts b/src/tests/unit/domain/use-cases/auth/get-current-user-use-case.test.ts
new file mode 100644
index 0000000..6710e13
--- /dev/null
+++ b/src/tests/unit/domain/use-cases/auth/get-current-user-use-case.test.ts
@@ -0,0 +1,86 @@
+import { mock } from 'jest-mock-extended';
+
+import { User } from '@/domain/models/user';
+import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
+import type { GetCurrentUserUseCasePayload } from '@/domain/use-cases/auth/get-current-user-use-case';
+import { GetCurrentUserUseCase } from '@/domain/use-cases/auth/get-current-user-use-case';
+import { Failure, Success } from '@/core/result/result';
+
+describe('GetCurrentUserUseCase', () => {
+ test('Call authenticator and send success response', async () => {
+ const authenticator = mock();
+
+ const user = new User({
+ id: 'test_id',
+ email: 'test@test.com',
+ username: 'test_username',
+ hashPassword: 'test_hash_password',
+ createdAt: new Date('2021-01-01T00:00:00.000Z'),
+ updatedAt: new Date('2021-01-01T00:00:00.000Z'),
+ });
+
+ authenticator.getAuthenticatedUser.mockResolvedValue({
+ success: true,
+ user,
+ });
+
+ const useCase = new GetCurrentUserUseCase(authenticator);
+
+ const payload: GetCurrentUserUseCasePayload = {
+ token: 'test_token',
+ };
+
+ const result = await useCase.execute(payload);
+
+ expect(authenticator.getAuthenticatedUser).toHaveBeenCalledWith(payload.token);
+ expect(result).toStrictEqual(new Success({
+ user,
+ }));
+ });
+
+ test('Call authenticator and send failure response if token is invalid', async () => {
+ const authenticator = mock();
+
+ authenticator.getAuthenticatedUser.mockResolvedValue({
+ success: false,
+ reason: 'InvalidToken',
+ });
+
+ const useCase = new GetCurrentUserUseCase(authenticator);
+
+ const payload: GetCurrentUserUseCasePayload = {
+ token: 'test_token',
+ };
+
+ const result = await useCase.execute(payload);
+
+ expect(authenticator.getAuthenticatedUser).toHaveBeenCalledWith(payload.token);
+ expect(result).toStrictEqual(new Failure({
+ reason: 'InvalidToken',
+ error: new Error('Failed to get current user: InvalidToken'),
+ }));
+ });
+
+ test('Call authenticator and send failure response if user is not found', async () => {
+ const authenticator = mock();
+
+ authenticator.getAuthenticatedUser.mockResolvedValue({
+ success: false,
+ reason: 'UserNotFound',
+ });
+
+ const useCase = new GetCurrentUserUseCase(authenticator);
+
+ const payload: GetCurrentUserUseCasePayload = {
+ token: 'test_token',
+ };
+
+ const result = await useCase.execute(payload);
+
+ expect(authenticator.getAuthenticatedUser).toHaveBeenCalledWith(payload.token);
+ expect(result).toStrictEqual(new Failure({
+ reason: 'UserNotFound',
+ error: new Error('Failed to get current user: UserNotFound'),
+ }));
+ });
+});
diff --git a/src/tests/unit/domain/use-cases/auth/login-use-case.test.ts b/src/tests/unit/domain/use-cases/auth/login-use-case.test.ts
new file mode 100644
index 0000000..e50343f
--- /dev/null
+++ b/src/tests/unit/domain/use-cases/auth/login-use-case.test.ts
@@ -0,0 +1,66 @@
+import { mock } from 'jest-mock-extended';
+
+import { Failure, Success } from '@/core/result/result';
+import { User } from '@/domain/models/user';
+import type { IAuthenticator } from '@/domain/services/auth/authenticator.interface';
+import type { LoginUseCasePayload } from '@/domain/use-cases/auth/login-use-case';
+import { LoginUseCase } from '@/domain/use-cases/auth/login-use-case';
+
+describe('LoginUseCase', () => {
+ test('Call authenticator and send success response', async () => {
+ const authenticator = mock();
+
+ const user = new User({
+ id: 'test_id',
+ email: 'test@test.com',
+ username: 'test_username',
+ hashPassword: 'test_hash_password',
+ createdAt: new Date('2021-01-01T00:00:00.000Z'),
+ updatedAt: new Date('2021-01-01T00:00:00.000Z'),
+ });
+
+ authenticator.authenticate.mockResolvedValue({
+ success: true,
+ user,
+ token: 'test_token',
+ });
+
+ const useCase = new LoginUseCase(authenticator);
+
+ const payload: LoginUseCasePayload = {
+ email: 'test@test.com',
+ password: 'test_password',
+ };
+
+ const result = await useCase.execute(payload);
+
+ expect(authenticator.authenticate).toHaveBeenCalledWith(payload.email, payload.password);
+ expect(result).toStrictEqual(new Success({
+ user,
+ token: 'test_token',
+ }));
+ });
+
+ test('Call authenticator and send failure response if credentials are invalid', async () => {
+ const authenticator = mock();
+
+ authenticator.authenticate.mockResolvedValue({
+ success: false,
+ });
+
+ const useCase = new LoginUseCase(authenticator);
+
+ const payload: LoginUseCasePayload = {
+ email: 'test@test.com',
+ password: 'test_password',
+ };
+
+ const result = await useCase.execute(payload);
+
+ expect(authenticator.authenticate).toHaveBeenCalledWith(payload.email, payload.password);
+ expect(result).toStrictEqual(new Failure({
+ reason: 'InvalidCredentials',
+ error: new Error('Invalid credentials'),
+ }));
+ });
+});
\ No newline at end of file
diff --git a/src/tests/unit/domain/use-cases/user/create-user-use-case.test.ts b/src/tests/unit/domain/use-cases/user/create-user-use-case.test.ts
index 433a239..5c5e13a 100644
--- a/src/tests/unit/domain/use-cases/user/create-user-use-case.test.ts
+++ b/src/tests/unit/domain/use-cases/user/create-user-use-case.test.ts
@@ -6,7 +6,7 @@ import type { ITime } from '@/core/time/time.interface';
import { User } from '@/domain/models/user';
import type { IUserRepository } from '@/domain/repositories/user-repository.interface';
import type { IEncryptor } from '@/domain/services/security/encryptor.interface';
-import type { CreateUserCasePayload } from '@/domain/use-cases/user/create-user-use-case';
+import type { CreateUserUseCasePayload } from '@/domain/use-cases/user/create-user-use-case';
import { CreateUserUseCase } from '@/domain/use-cases/user/create-user-use-case';
describe('CreateUserUseCase', () => {
@@ -34,7 +34,7 @@ describe('CreateUserUseCase', () => {
const useCase = new CreateUserUseCase(idGenerator, time, encryptor, userRepository);
- const payload: CreateUserCasePayload = {
+ const payload: CreateUserUseCasePayload = {
email: 'test@test.com',
username: 'test_username',
password: 'test_password',
@@ -57,7 +57,7 @@ describe('CreateUserUseCase', () => {
const useCase = new CreateUserUseCase(idGenerator, time, encryptor, userRepository);
- const payload: CreateUserCasePayload = {
+ const payload: CreateUserUseCasePayload = {
email: 'test@test.com',
username: 'test_username',
password: 'test_password',
@@ -81,7 +81,7 @@ describe('CreateUserUseCase', () => {
const useCase = new CreateUserUseCase(idGenerator, time, encryptor, userRepository);
- const payload: CreateUserCasePayload = {
+ const payload: CreateUserUseCasePayload = {
email: 'test@test.com',
username: 'test_username',
password: 'test_password',
diff --git a/yarn.lock b/yarn.lock
index 1043a01..c998e0f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -834,7 +834,7 @@
"@types/range-parser" "*"
"@types/send" "*"
-"@types/express-session@*", "@types/express-session@1.18.1":
+"@types/express-session@*":
version "1.18.1"
resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.18.1.tgz#67c629a34b60a63a4724f359aac0c0e6d1f15365"
integrity sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==
@@ -900,6 +900,14 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
+"@types/jsonwebtoken@^9.0.9":
+ version "9.0.9"
+ resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz#a4c3a446c0ebaaf467a58398382616f416345fb3"
+ integrity sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==
+ dependencies:
+ "@types/ms" "*"
+ "@types/node" "*"
+
"@types/methods@^1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547"
@@ -917,6 +925,11 @@
dependencies:
"@types/node" "*"
+"@types/ms@*":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78"
+ integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
+
"@types/node@*":
version "22.10.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9"
@@ -931,30 +944,6 @@
dependencies:
undici-types "~6.20.0"
-"@types/passport-local@1.0.38":
- version "1.0.38"
- resolved "https://registry.yarnpkg.com/@types/passport-local/-/passport-local-1.0.38.tgz#8073758188645dde3515808999b1c218a6fe7141"
- integrity sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==
- dependencies:
- "@types/express" "*"
- "@types/passport" "*"
- "@types/passport-strategy" "*"
-
-"@types/passport-strategy@*":
- version "0.2.38"
- resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.38.tgz#482abba0b165cd4553ec8b748f30b022bd6c04d3"
- integrity sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==
- dependencies:
- "@types/express" "*"
- "@types/passport" "*"
-
-"@types/passport@*", "@types/passport@1.0.17":
- version "1.0.17"
- resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.17.tgz#718a8d1f7000ebcf6bbc0853da1bc8c4bc7ea5e6"
- integrity sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==
- dependencies:
- "@types/express" "*"
-
"@types/pg@*":
version "8.11.10"
resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.10.tgz#b8fb2b2b759d452fe3ec182beadd382563b63291"
@@ -1462,6 +1451,11 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
+buffer-equal-constant-time@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+ integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
+
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@@ -1638,13 +1632,6 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
-connect-pg-simple@10.0.0:
- version "10.0.0"
- resolved "https://registry.yarnpkg.com/connect-pg-simple/-/connect-pg-simple-10.0.0.tgz#972b08d9fc6a1861c523a6c9166240a24b4bc3ca"
- integrity sha512-pBGVazlqiMrackzCr0eKhn4LO5trJXsOX0nQoey9wCOayh80MYtThCbq8eoLsjpiWgiok/h+1/uti9/2/Una8A==
- dependencies:
- pg "^8.12.0"
-
content-disposition@0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
@@ -1667,21 +1654,11 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
-cookie-signature@1.0.7:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454"
- integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==
-
cookie@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9"
integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==
-cookie@0.7.2:
- version "0.7.2"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
- integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
-
cookiejar@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b"
@@ -1879,6 +1856,13 @@ eastasianwidth@^0.2.0:
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
+ecdsa-sig-formatter@1.0.11:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
+ integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
+ dependencies:
+ safe-buffer "^5.0.1"
+
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -2240,20 +2224,6 @@ expect@^29.0.0, expect@^29.7.0:
jest-message-util "^29.7.0"
jest-util "^29.7.0"
-express-session@1.18.1:
- version "1.18.1"
- resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.1.tgz#88d0bbd41878882840f24ec6227493fcb167e8d5"
- integrity sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==
- dependencies:
- cookie "0.7.2"
- cookie-signature "1.0.7"
- debug "2.6.9"
- depd "~2.0.0"
- on-headers "~1.0.2"
- parseurl "~1.3.3"
- safe-buffer "5.2.1"
- uid-safe "~2.1.5"
-
express@4.21.2:
version "4.21.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32"
@@ -3491,6 +3461,39 @@ json5@^2.2.2, json5@^2.2.3:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+jsonwebtoken@^9.0.2:
+ version "9.0.2"
+ resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3"
+ integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==
+ dependencies:
+ jws "^3.2.2"
+ lodash.includes "^4.3.0"
+ lodash.isboolean "^3.0.3"
+ lodash.isinteger "^4.0.4"
+ lodash.isnumber "^3.0.3"
+ lodash.isplainobject "^4.0.6"
+ lodash.isstring "^4.0.1"
+ lodash.once "^4.0.0"
+ ms "^2.1.1"
+ semver "^7.5.4"
+
+jwa@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
+ integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
+ dependencies:
+ buffer-equal-constant-time "1.0.1"
+ ecdsa-sig-formatter "1.0.11"
+ safe-buffer "^5.0.1"
+
+jws@^3.2.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
+ integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
+ dependencies:
+ jwa "^1.4.1"
+ safe-buffer "^5.0.1"
+
keyv@^4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -3535,6 +3538,36 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
+lodash.includes@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
+ integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==
+
+lodash.isboolean@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
+ integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
+
+lodash.isinteger@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
+ integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==
+
+lodash.isnumber@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
+ integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==
+
+lodash.isplainobject@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+ integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
+
+lodash.isstring@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
+ integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
+
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -3545,6 +3578,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+lodash.once@^4.0.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
+ integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
+
lru-cache@^10.2.0:
version "10.4.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
@@ -3952,27 +3990,6 @@ parseurl@~1.3.3:
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
-passport-local@1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee"
- integrity sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==
- dependencies:
- passport-strategy "1.x.x"
-
-passport-strategy@1.x.x:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
- integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==
-
-passport@0.7.0:
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/passport/-/passport-0.7.0.tgz#3688415a59a48cf8068417a8a8092d4492ca3a05"
- integrity sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==
- dependencies:
- passport-strategy "1.x.x"
- pause "0.0.1"
- utils-merge "^1.0.1"
-
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -4011,11 +4028,6 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
-pause@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
- integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==
-
pg-cloudflare@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98"
@@ -4070,7 +4082,7 @@ pg-types@^4.0.1:
postgres-interval "^3.0.0"
postgres-range "^1.1.1"
-pg@8.13.1, pg@^8.12.0:
+pg@8.13.1:
version "8.13.1"
resolved "https://registry.yarnpkg.com/pg/-/pg-8.13.1.tgz#6498d8b0a87ff76c2df7a32160309d3168c0c080"
integrity sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==
@@ -4247,11 +4259,6 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
-random-bytes@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
- integrity sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==
-
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@@ -4989,13 +4996,6 @@ typescript@5.7.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
-uid-safe@~2.1.5:
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
- integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==
- dependencies:
- random-bytes "~1.0.0"
-
unbox-primitive@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2"
@@ -5036,7 +5036,7 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
-utils-merge@1.0.1, utils-merge@^1.0.1:
+utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==