feat(api): health checks
This commit is contained in:
parent
207d3483ca
commit
376c0fd041
3
TODO.md
3
TODO.md
@ -34,8 +34,7 @@
|
|||||||
- [x] Implement `GET /wikipedia/pages?title=Node.js` to search a page by title (not necessarily with the title sanitized, search with input by user to check if page exists)
|
- [x] Implement `GET /wikipedia/pages?title=Node.js` to search a page by title (not necessarily with the title sanitized, search with input by user to check if page exists)
|
||||||
- [ ] Implement `GET /wikipedia/pages/internal-links/paths?from=Node.js&to=Linux` to get all the possible paths between 2 pages with titles sanitized
|
- [ ] Implement `GET /wikipedia/pages/internal-links/paths?from=Node.js&to=Linux` to get all the possible paths between 2 pages with titles sanitized
|
||||||
- [x] Setup tests with database + add coverage
|
- [x] Setup tests with database + add coverage
|
||||||
- [ ] Setup HTTP Requests logging in development (not needed in `test` mode)
|
- [x] Setup Health checks
|
||||||
- [ ] Setup Health checks
|
|
||||||
- [x] Setup Rate limiting
|
- [x] Setup Rate limiting
|
||||||
- [ ] Share VineJS validators between `website` and `api`
|
- [ ] Share VineJS validators between `website` and `api`
|
||||||
- [ ] Implement Wikipedia Game Solver (`website`)
|
- [ ] Implement Wikipedia Game Solver (`website`)
|
||||||
|
12
apps/api/src/app/middleware/app_key_security_middleware.ts
Normal file
12
apps/api/src/app/middleware/app_key_security_middleware.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { APP_KEY, APP_KEY_HEADER_NAME } from "#config/app.js"
|
||||||
|
import type { HttpContext } from "@adonisjs/core/http"
|
||||||
|
import type { NextFn } from "@adonisjs/core/types/http"
|
||||||
|
|
||||||
|
export default class AppKeySecurityMiddleware {
|
||||||
|
public async handle(context: HttpContext, next: NextFn): Promise<void> {
|
||||||
|
if (context.request.header(APP_KEY_HEADER_NAME) === APP_KEY) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
return context.response.unauthorized({ message: "Unauthorized access" })
|
||||||
|
}
|
||||||
|
}
|
@ -3,12 +3,11 @@ import type { HttpContext } from "@adonisjs/core/http"
|
|||||||
import type { NextFn } from "@adonisjs/core/types/http"
|
import type { NextFn } from "@adonisjs/core/types/http"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auth middleware is used authenticate HTTP requests and deny
|
* Auth middleware is used authenticate HTTP requests and deny access to unauthenticated users.
|
||||||
* access to unauthenticated users.
|
|
||||||
*/
|
*/
|
||||||
export default class AuthMiddleware {
|
export default class AuthMiddleware {
|
||||||
/**
|
/**
|
||||||
* The URL to redirect to, when authentication fails
|
* The URL to redirect to, when authentication fails.
|
||||||
*/
|
*/
|
||||||
redirectTo = "/login"
|
redirectTo = "/login"
|
||||||
|
|
||||||
|
16
apps/api/src/app/routes/health/get.ts
Normal file
16
apps/api/src/app/routes/health/get.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { healthChecks } from "#start/health.js"
|
||||||
|
import { middleware } from "#start/kernel.js"
|
||||||
|
import type { HttpContext } from "@adonisjs/core/http"
|
||||||
|
import router from "@adonisjs/core/services/router"
|
||||||
|
|
||||||
|
class Controller {
|
||||||
|
public async handle(context: HttpContext): Promise<void> {
|
||||||
|
const report = await healthChecks.run()
|
||||||
|
if (report.isHealthy) {
|
||||||
|
return context.response.ok(report)
|
||||||
|
}
|
||||||
|
return context.response.serviceUnavailable(report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get("/health", [Controller]).use(middleware.appKeySecurity())
|
33
apps/api/src/app/routes/health/tests/get.test.ts
Normal file
33
apps/api/src/app/routes/health/tests/get.test.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { APP_KEY, APP_KEY_HEADER_NAME } from "#config/app.js"
|
||||||
|
import { test } from "@japa/runner"
|
||||||
|
|
||||||
|
test.group("GET /health", () => {
|
||||||
|
test("should succeeds and get `isHealthy: true`", async ({
|
||||||
|
client,
|
||||||
|
assert,
|
||||||
|
}) => {
|
||||||
|
// Arrange - Given
|
||||||
|
|
||||||
|
// Act - When
|
||||||
|
const response = await client
|
||||||
|
.get("/health")
|
||||||
|
.header(APP_KEY_HEADER_NAME, APP_KEY)
|
||||||
|
const responseBody = response.body()
|
||||||
|
|
||||||
|
// Assert - Then
|
||||||
|
response.assertStatus(200)
|
||||||
|
assert.equal(responseBody.isHealthy, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should fails and unauthorized when the app key is not provided", async ({
|
||||||
|
client,
|
||||||
|
}) => {
|
||||||
|
// Arrange - Given
|
||||||
|
|
||||||
|
// Act - When
|
||||||
|
const response = await client.get("/health")
|
||||||
|
|
||||||
|
// Assert - Then
|
||||||
|
response.assertStatus(401)
|
||||||
|
})
|
||||||
|
})
|
@ -1,2 +1,3 @@
|
|||||||
import "#app/routes/get.js"
|
import "#app/routes/get.js"
|
||||||
|
import "#app/routes/health/get.js"
|
||||||
import "#app/routes/wikipedia/index.js"
|
import "#app/routes/wikipedia/index.js"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { test } from "@japa/runner"
|
import { test } from "@japa/runner"
|
||||||
|
|
||||||
test.group("GET /", () => {
|
test.group("GET /", () => {
|
||||||
test("should get hello world", async ({ client }) => {
|
test("should succeeds and get hello world", async ({ client }) => {
|
||||||
// Arrange - Given
|
// Arrange - Given
|
||||||
|
|
||||||
// Act - When
|
// Act - When
|
||||||
|
@ -51,6 +51,7 @@ test.group("GET /wikipedia/pages", (group) => {
|
|||||||
const response = await client.get(
|
const response = await client.get(
|
||||||
`/wikipedia/pages?${searchParams.toString()}`,
|
`/wikipedia/pages?${searchParams.toString()}`,
|
||||||
)
|
)
|
||||||
|
const responseBody = response.body()
|
||||||
|
|
||||||
// Assert - Then
|
// Assert - Then
|
||||||
response.assertStatus(200)
|
response.assertStatus(200)
|
||||||
@ -59,7 +60,7 @@ test.group("GET /wikipedia/pages", (group) => {
|
|||||||
return page.toJSON()
|
return page.toJSON()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
assert.equal(response.body().length, limit)
|
assert.equal(responseBody.length, limit)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should fails when "title" is not provided', async ({ client }) => {
|
test('should fails when "title" is not provided', async ({ client }) => {
|
||||||
|
@ -9,7 +9,9 @@ import app from "@adonisjs/core/services/app"
|
|||||||
* The encryption module will fail to decrypt data if the key is lost or changed.
|
* The encryption module will fail to decrypt data if the key is lost or changed.
|
||||||
* Therefore it is recommended to keep the app key secure.
|
* Therefore it is recommended to keep the app key secure.
|
||||||
*/
|
*/
|
||||||
export const appKey = new Secret(env.get("APP_KEY"))
|
export const APP_KEY_HEADER_NAME = "x-app-key"
|
||||||
|
export const APP_KEY = env.get("APP_KEY")
|
||||||
|
export const appKey = new Secret(APP_KEY)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The configuration settings used by the HTTP server
|
* The configuration settings used by the HTTP server
|
||||||
|
13
apps/api/src/start/health.ts
Normal file
13
apps/api/src/start/health.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {
|
||||||
|
DiskSpaceCheck,
|
||||||
|
HealthChecks,
|
||||||
|
MemoryHeapCheck,
|
||||||
|
} from "@adonisjs/core/health"
|
||||||
|
import { DbCheck } from "@adonisjs/lucid/database"
|
||||||
|
import db from "@adonisjs/lucid/services/db"
|
||||||
|
|
||||||
|
export const healthChecks = new HealthChecks().register([
|
||||||
|
new DiskSpaceCheck(),
|
||||||
|
new MemoryHeapCheck(),
|
||||||
|
new DbCheck(db.connection()),
|
||||||
|
])
|
@ -45,6 +45,9 @@ router.use([
|
|||||||
* Named middleware collection must be explicitly assigned to the routes or the routes group.
|
* Named middleware collection must be explicitly assigned to the routes or the routes group.
|
||||||
*/
|
*/
|
||||||
export const middleware = router.named({
|
export const middleware = router.named({
|
||||||
|
appKeySecurity: async () => {
|
||||||
|
return await import("#app/middleware/app_key_security_middleware.js")
|
||||||
|
},
|
||||||
auth: async () => {
|
auth: async () => {
|
||||||
return await import("#app/middleware/auth_middleware.js")
|
return await import("#app/middleware/auth_middleware.js")
|
||||||
},
|
},
|
||||||
|
@ -4,11 +4,12 @@
|
|||||||
* The "limiter.define" method creates an HTTP middleware to apply rate limits on a route or a group of routes. Feel free to define as many throttle middleware as needed.
|
* The "limiter.define" method creates an HTTP middleware to apply rate limits on a route or a group of routes. Feel free to define as many throttle middleware as needed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { APP_KEY, APP_KEY_HEADER_NAME } from "#config/app.js"
|
||||||
import app from "@adonisjs/core/services/app"
|
import app from "@adonisjs/core/services/app"
|
||||||
import limiter from "@adonisjs/limiter/services/main"
|
import limiter from "@adonisjs/limiter/services/main"
|
||||||
|
|
||||||
export const throttle = limiter.define("global", () => {
|
export const throttle = limiter.define("global", (context) => {
|
||||||
if (app.inTest) {
|
if (app.inTest || context.request.header(APP_KEY_HEADER_NAME) === APP_KEY) {
|
||||||
return limiter.noLimit()
|
return limiter.noLimit()
|
||||||
}
|
}
|
||||||
return limiter.allowRequests(120).every("1 minute")
|
return limiter.allowRequests(120).every("1 minute")
|
||||||
|
Reference in New Issue
Block a user