-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented OAuth2 and pagination. General refactoring
- Loading branch information
1 parent
c643f37
commit e2014b3
Showing
32 changed files
with
221 additions
and
86 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,10 @@ | ||
## REST API Node Boilerplate | ||
# REST API Boilerplate for Node.js in TypeScript | ||
|
||
### Base services setup | ||
#### A simple REST API boilerplate created using Hapi, Boom, Joiful, Pagination, Swagger and OAuth2. | ||
|
||
> Warning: for production ready environments change this part as you prefer. This is a simple example, setting up a strong OAuth2 authorization server. Advanced configurations are out of the scope of this guide | ||
### Base services setup | ||
|
||
Services spawned in this demo are: | ||
- `mysql` database | ||
- `postgresql` database | ||
- `hydra-migrate` | ||
> Warning: for production ready environments change this part as you prefer. This is a simple example, setup of a strong OAuth2 authorization server and advanced configurations are out of the scope of this guide | ||
Generate OAuth2 server secret: | ||
```bash | ||
|
@@ -28,7 +25,7 @@ Stop all services with: | |
$ docker-compose down | ||
``` | ||
|
||
Create your first client: | ||
Create your first OAuth client: | ||
```bash | ||
$ docker exec hydra \ | ||
hydra clients create \ | ||
|
@@ -40,7 +37,7 @@ $ docker exec hydra \ | |
--callbacks http://localhost:3000/api/oauth/authorize | ||
``` | ||
|
||
Deploy a sample Login & Consent App: | ||
Deploy a sample Login & Consent OAuth App: | ||
```bash | ||
$ docker run -d \ | ||
--name hydra-consent \ | ||
|
@@ -51,31 +48,31 @@ $ docker run -d \ | |
oryd/hydra-login-consent-node:v1.3.2 | ||
``` | ||
|
||
Create first user: | ||
Create first user in database: | ||
```bash | ||
$ npm run seed | ||
``` | ||
|
||
### Configure environement | ||
### Configure environment | ||
|
||
Set `.env` dev environement: | ||
Set `.env` dev environment: | ||
```bash | ||
$ cp .env.dev .env | ||
``` | ||
|
||
Set `.env` production environement: | ||
Set `.env` production environment: | ||
```bash | ||
$ cp .env.prod .env | ||
``` | ||
|
||
### Prisma client | ||
|
||
Prisma migration in dev | ||
Prisma migration in dev: | ||
```bash | ||
$ npx prisma migrate dev --name init | ||
``` | ||
|
||
Prisma migration in prod | ||
Prisma migration in prod: | ||
```bash | ||
$ npx prisma migrate deploy | ||
``` | ||
|
@@ -92,6 +89,62 @@ To introspect database use: | |
$ npx prisma studio | ||
``` | ||
|
||
### Endpoints: | ||
|
||
REST API endpoint: | ||
``` | ||
http://localhost:3000/api | ||
``` | ||
|
||
REST API endpoint: | ||
``` | ||
http://localhost:3000/api | ||
``` | ||
|
||
### Workflow | ||
|
||
Follow these steps to complete the setup | ||
|
||
1. Prepare environment: | ||
```bash | ||
$ cp .env.dev .env | ||
$ docker-compose up -d | ||
$ npx prisma migrate dev --name init | ||
$ npm run seed | ||
$ npm run dev | ||
``` | ||
|
||
2. Go to swagger endpoint: | ||
``` | ||
http://localhost:3000/api-docs#/ | ||
``` | ||
|
||
3. Use default credentials: | ||
``` | ||
Username: admin | ||
Password: admin | ||
``` | ||
|
||
4. Make a `GET` request to `/api/oauth/authenticate` | ||
|
||
5. Copy the `authUrl` in the response body and open it in a new browser window | ||
|
||
6. Login to sample app with credentials `[email protected]` as email, `foobar` as password and give consent | ||
|
||
7. After authentication completed, copy the JWT token provided and use it to authorize REST API requests as JWT Bearer token | ||
|
||
### Token refresh lifecycle | ||
|
||
In order to prevent access token expiration issues an access token lifecycle has been implemented to refresh it when it expires. | ||
If this situation occurs the current request is authorized anyways but starting from next one you should provide newly generated access token, coming back in the `Authorization` header of the current response. | ||
|
||
> This allows full transparency of access token expiration and refresh to end-user using the API | ||
### Pull requests and Issues | ||
|
||
Feel free to submit issues and pull requests if you want :smile: | ||
|
||
### TODO | ||
|
||
> Write tests | ||
- [ ] Write tests | ||
- [ ] Checks other TODOs in code |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 8 additions & 10 deletions
18
src/plugins/core/oauth2.ts → src/core/controllers/oauth2.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
|
||
export default interface IPageResponse<T> { | ||
totalElements: number | ||
totalPages: number | ||
currentPage: number | ||
pageSize: number | ||
content: T[] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import {BaseModel} from "../shared/base-model"; | ||
import {number} from "joiful"; | ||
|
||
export default class PageRequest extends BaseModel { | ||
|
||
@number().optional().min(0) | ||
pageIndex?: number | ||
|
||
@number().optional().min(1) | ||
pageSize?: number | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import {BaseModel} from "../shared/base-model" | ||
import boom from '@hapi/boom' | ||
import IPageResponse from "./interfaces/ipage-response"; | ||
import PageRequest from "./page-request"; | ||
|
||
export default class Paginator<T> { | ||
|
||
static DEFAULT_PAGE_SIZE = 25 | ||
|
||
model: typeof BaseModel | ||
|
||
// TODO define type for field `prismaModelDelegate` | ||
prismaModelDelegate: any | ||
|
||
totalElements: number | ||
|
||
totalPages: number | ||
|
||
currentPage: number | ||
|
||
pageSize: number | ||
|
||
content: T[] | ||
|
||
constructor(model: typeof BaseModel, prismaModelDelegate: any, pageRequest?: PageRequest) { | ||
this.model = model | ||
this.prismaModelDelegate = prismaModelDelegate | ||
this.totalElements = 0 | ||
this.totalPages = 1 | ||
this.currentPage = pageRequest?.pageIndex || 0 | ||
this.pageSize = pageRequest?.pageSize || Paginator.DEFAULT_PAGE_SIZE | ||
this.content = [] | ||
} | ||
|
||
async getPage(extraQueryParams?: object): Promise<IPageResponse<T>> { | ||
await this.fetchElements(extraQueryParams) | ||
|
||
// Out of pages exception | ||
if (this.currentPage > this.totalPages - 1) { | ||
throw boom.badRequest('Current page is greater than total pages') | ||
} | ||
|
||
return { | ||
content: this.content, | ||
pageSize: this.pageSize, | ||
currentPage: this.currentPage, | ||
totalElements: this.totalElements, | ||
totalPages: this.totalPages, | ||
} as IPageResponse<T> | ||
} | ||
|
||
private async fetchElements(extraQueryParams?: object): Promise<void> { | ||
|
||
// Count total elements | ||
this.totalElements = await this.getTotalElements() | ||
|
||
// Get totale pages | ||
this.totalPages = await this.getTotalPages() | ||
|
||
// Fetch elements for current page | ||
this.content = (await this.prismaModelDelegate.findMany({ | ||
take: this.pageSize, | ||
skip: this.pageSize * this.currentPage, | ||
...extraQueryParams | ||
})).map((data: object) => this.model.fromJSON<T>(data)) | ||
|
||
} | ||
|
||
private async getTotalElements(): Promise<number> { | ||
return await this.prismaModelDelegate.count() | ||
} | ||
|
||
private async getTotalPages(): Promise<number> { | ||
return Math.ceil(await this.getTotalElements() / this.pageSize) | ||
} | ||
|
||
} |
File renamed without changes.
4 changes: 2 additions & 2 deletions
4
src/plugins/core/prisma.ts → src/core/plugins/prisma/prisma.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion
2
...positories/core/oauth2/oauth2-provider.ts → src/core/providers/oauth2-provider.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.