This project which applies MVC/DDD/Hexagonal structure is using Express, MongoDB and Mongoose as ORM. The project has an implementation of an authentication system that uses JSON Web Token to manage admin' login data in Node.js web server and user management by admin.
-
Architecture Overview 1.1 Data Layer 1.2 Domain Layer 1.3 Routs/Controller Layer
-
Security Concern 2.1 JWT signing and verification 2.2 Password Hashing 2.3 Username and Email Encryption
-
Quick Start 3.1 Prerequisites 3.2 Use Docker 3.3 Use the npm scripts
-
Endpoints 4.1 Admin Routes 4.2 User Routs
The app is designed to use a layered architecture. The architecture is heavily influenced by the Clean Architecture.Clean Architecture is an architecture where:
- does not depend on the existence of some framework, database, external agency.
- does not depend on UI
- the business rules can be tested without the UI, database, web server, or any external element.
Also, in entry point(server.js), I use Dependency Injection(DI). There are many reasons using Dependency Injection as:
- Decoupling
- Easier unit testing
- Faster development
- Dependency injection is really helpful when it comes to testing. You can easily mock your modules' dependencies using this pattern.
According to DI: A. High-level modules should not depend on low-level modules. Both should depend on abstractions. B. Abstractions should not depend on details.
The code style being used is based on the airbnb js style guide.
The data layer is implemented using repositories, that hide the underlying data sources (database, network, cache, etc), and provides an abstraction over them so other parts of the application that make use of the repositories, don't care about the origin of the data and are decoupled from the specific implementations used, like the Mongoose ORM that is used by this app. Furthermore, the repositories are responsible to map the entities they fetch from the data sources to the models used in the applications. This is important to enable the decoupling.
The domain layer is implemented using services. They depend on the repositories to get the app models and apply the business rules on them. They are not coupled to a specific database implementation and can be reused if we add more data sources to the app or even if we change the database for example from MongoDB to Couchbase Server.
This layer is being used in the express app and depends on the domain layer (services). Here we define the routes that can be called from outside. The services are always used as the last middleware on the routes and we must not rely on res.locals from express to get data from previous middlewares. That means that the middlewares registered before should not alter data being passed to the domain layer. They are only allowed to act upon the data without modification, like for example validating the data and skipping calling next().
The entry point for the applications is the server.js file. It does not depend on express.js or other node.js frameworks. It is responsible for instantiating the application layers, connecting to the db and mounting the http server to the specified port.
As a default, JWT uses HMAC-SHA256 algorithm for signing and verifying. While SHA256 is one of the most popular one, it has security concerns so that it is hardly recommended for a top security application. In this project, RS512 is used for signing and verifying JWT.
Generate a keypair (private key and public key) and save it as private.key and public.key.
private.pem
-----BEGIN PRIVATE KEY-----
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDpLtqxS7OrlD/d
T2tuz4+QNUh2OCa2Bat4bmpY+wL3FdkqIxXUCJX0tfKpCwBikKoQMzddt+ZmoZvj
zIuFv9eploqBJhoL+HYOMzuWCshACn33TZGvx9SYs3aK+vm2cvFRQ6cw5zZJC2v1
2DNM41hblm7c/DK8BaTkPq54hSEu1jOlwH562g10vcivbvjoojL9VSwPAAzt2Gup
IrxTbEUIaVq7iKQ5O2/MOjCcAwcyt8TurUHpZlAMBCUGbFFCzIqWfkMiwq/rFq42
wdGAEApy1TFkbwzhAkjHdLoC6CF3dFkLgJrkB7193wvyaU1gEKtCE5nt1LR/hq3h
quUtxqO3AgMBAAECggEBANX6C+7EA/TADrbcCT7fMuNnMb5iGovPuiDCWc6bUIZC
Q0yac45l7o1nZWzfzpOkIprJFNZoSgIF7NJmQeYTPCjAHwsSVraDYnn3Y4d1D3tM
5XjJcpX2bs1NactxMTLOWUl0JnkGwtbWp1Qq+DBnMw6ghc09lKTbHQvhxSKNL/0U
C+YmCYT5ODmxzLBwkzN5RhxQZNqol/4LYVdji9bS7N/UITw5E6LGDOo/hZHWqJsE
fgrJTPsuCyrYlwrNkgmV2KpRrGz5MpcRM7XHgnqVym+HyD/r9E7MEFdTLEaiiHcm
Ish1usJDEJMFIWkF+rnEoJkQHbqiKlQBcoqSbCmoMWECgYEA/4379mMPF0JJ/EER
4VH7/ZYxjdyphenx2VYCWY/uzT0KbCWQF8KXckuoFrHAIP3EuFn6JNoIbja0NbhI
HGrU29BZkATG8h/xjFy/zPBauxTQmM+yS2T37XtMoXNZNS/ubz2lJXMOapQQiXVR
l/tzzpyWaCe9j0NT7DAU0ZFmDbECgYEA6ZbjkcOs2jwHsOwwfamFm4VpUFxYtED7
9vKzq5d7+Ii1kPKHj5fDnYkZd+mNwNZ02O6OGxh40EDML+i6nOABPg/FmXeVCya9
Vump2Yqr2fAK3xm6QY5KxAjWWq2kVqmdRmICSL2Z9rBzpXmD5o06y9viOwd2bhBo
0wB02416GecCgYEA+S/ZoEa3UFazDeXlKXBn5r2tVEb2hj24NdRINkzC7h23K/z0
pDZ6tlhPbtGkJodMavZRk92GmvF8h2VJ62vAYxamPmhqFW5Qei12WL+FuSZywI7F
q/6oQkkYT9XKBrLWLGJPxlSKmiIGfgKHrUrjgXPutWEK1ccw7f10T2UXvgECgYEA
nXqLa58G7o4gBUgGnQFnwOSdjn7jkoppFCClvp4/BtxrxA+uEsGXMKLYV75OQd6T
IhkaFuxVrtiwj/APt2lRjRym9ALpqX3xkiGvz6ismR46xhQbPM0IXMc0dCeyrnZl
QKkcrxucK/Lj1IBqy0kVhZB1IaSzVBqeAPrCza3AzqsCgYEAvSiEjDvGLIlqoSvK
MHEVe8PBGOZYLcAdq4YiOIBgddoYyRsq5bzHtTQFgYQVK99Cnxo+PQAvzGb+dpjN
/LIEAS2LuuWHGtOrZlwef8ZpCQgrtmp/phXfVi6llcZx4mMm7zYmGhh2AsA9yEQc
acgc4kgDThAjD7VlXad9UHpNMO8=
-----END PRIVATE KEY-----
public.key
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6S7asUuzq5Q/3U9rbs+P
kDVIdjgmtgWreG5qWPsC9xXZKiMV1AiV9LXyqQsAYpCqEDM3XbfmZqGb48yLhb/X
qZaKgSYaC/h2DjM7lgrIQAp9902Rr8fUmLN2ivr5tnLxUUOnMOc2SQtr9dgzTONY
W5Zu3PwyvAWk5D6ueIUhLtYzpcB+etoNdL3Ir2746KIy/VUsDwAM7dhrqSK8U2xF
CGlau4ikOTtvzDownAMHMrfE7q1B6WZQDAQlBmxRQsyKln5DIsKv6xauNsHRgBAK
ctUxZG8M4QJIx3S6Aughd3RZC4Ca5Ae9fd8L8mlNYBCrQhOZ7dS0f4at4arlLcaj
twIDAQAB
-----END PUBLIC KEY-----
Copy these two file to src/configuration directory.
One of the most secure algorithm bcrypt
was used for password hashing.
Before saving these data to database, these are encrypted using AES-256 so that we can ensure the privacy of data.
Create an .env file in project root to register the following required environment variables:
DATABASE_URL
- MongoDB connection URLHTTP_PORT
- port of serverJWT_SECRET
- we will use secret to generate our JSON web tokens
You can use Docker to start the app locally. The Dockerfile and the docker-compose.yml are already provided for you. You have to run the following command:
docker-compose up
npm run start
for running project.
npm run build
for building project.
npm run test
for running tests.
Register Admin if no admin exists
POST /admin/register
Body Params:
{
fullname,
email,
password
}
Description: creates a new admin. Password is stored in bcrypt format.
POST /admin/login
Body Params:
{
email,
password
}
Description: logs in to the server. Server will return a JWT token:
{
"status": "success",
"data": {
"token": {
access_token: "eyJhbGciOiJIUzxxxxxxx.eyJlbWFpbCI6ImRpbW9zdGhlbxxxxxxxxxxxxx.axxxxxxxxxx",
expiresIn: 86400,
},
}
}
GET /admin/info
{
"status": "success",
"data": {
"username": "admin",
"email": "super.admin@gmail.com",
"created": "2022-10-30T22:55:23.507Z"
}
}
Description: logs in to the server. Server will return a JWT token:
{
"status": "success",
"data": {
"token": {
access_token: "eyJhbGciOiJIUzxxxxxxx.eyJlbWFpbCI6ImRpbW9zdGhlbxxxxxxxxxxxxx.axxxxxxxxxx",
expiresIn: 86400,
},
}
}
PUT /admin/update
Body Params:
{
fullname,
email,
password
}
Description: Update admin info:
{
"status": "success",
"data": {
"id": "635f00db957b63a0db223a67",
"username": "group.user9",
"email": "grop.user9@gmail.com",
"created": "2022-10-30T22:55:23.507Z"
}
}
DELETE /admin/
Description: Delete admin :
In order to be able to retrieve posts list, user should send a Bearer token using Authorization header, otherwise server will answer with 401.
POST /users/
Body Params:
{
fullname {String},
email {String},
password {String}
}
Description: Create user with Admin's credential.
GET /users/:userId
Response is
{
"data": {
"id": "635f00db957b63a0db223a67",
"username": "group.user9",
"email": "grop.user9@gmail.com",
"created": "2022-10-30T22:55:23.507Z"
}
}
Description: Gets specific user.
GET /users/all
Description: Gets all users.
Response is
{
"data": [
{
"id": "635f00db957b63a0db223a67",
"username": "group.user9",
"email": "grop.user9@gmail.com",
"created": "2022-10-30T22:55:23.507Z"
},
]
}
PUT /users/:userId
Body Params:
{
fullname {String},
email {String},
password {String}
}
Description: Updatae a specific user with Admin's credential.
Delete /users/:userId
Description: Updatae a specific user with Admin's credential.
Please visit http://localhost:8080/docs for more details about endpoints.