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)
|
||||
- [ ] 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
|
||||
- [ ] Setup HTTP Requests logging in development (not needed in `test` mode)
|
||||
- [ ] Setup Health checks
|
||||
- [x] Setup Health checks
|
||||
- [x] Setup Rate limiting
|
||||
- [ ] Share VineJS validators between `website` and `api`
|
||||
- [ ] 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"
|
||||
|
||||
/**
|
||||
* Auth middleware is used authenticate HTTP requests and deny
|
||||
* access to unauthenticated users.
|
||||
* Auth middleware is used authenticate HTTP requests and deny access to unauthenticated users.
|
||||
*/
|
||||
export default class AuthMiddleware {
|
||||
/**
|
||||
* The URL to redirect to, when authentication fails
|
||||
* The URL to redirect to, when authentication fails.
|
||||
*/
|
||||
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/health/get.js"
|
||||
import "#app/routes/wikipedia/index.js"
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { test } from "@japa/runner"
|
||||
|
||||
test.group("GET /", () => {
|
||||
test("should get hello world", async ({ client }) => {
|
||||
test("should succeeds and get hello world", async ({ client }) => {
|
||||
// Arrange - Given
|
||||
|
||||
// Act - When
|
||||
|
@ -51,6 +51,7 @@ test.group("GET /wikipedia/pages", (group) => {
|
||||
const response = await client.get(
|
||||
`/wikipedia/pages?${searchParams.toString()}`,
|
||||
)
|
||||
const responseBody = response.body()
|
||||
|
||||
// Assert - Then
|
||||
response.assertStatus(200)
|
||||
@ -59,7 +60,7 @@ test.group("GET /wikipedia/pages", (group) => {
|
||||
return page.toJSON()
|
||||
}),
|
||||
)
|
||||
assert.equal(response.body().length, limit)
|
||||
assert.equal(responseBody.length, limit)
|
||||
})
|
||||
|
||||
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.
|
||||
* 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
|
||||
|
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.
|
||||
*/
|
||||
export const middleware = router.named({
|
||||
appKeySecurity: async () => {
|
||||
return await import("#app/middleware/app_key_security_middleware.js")
|
||||
},
|
||||
auth: async () => {
|
||||
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.
|
||||
*/
|
||||
|
||||
import { APP_KEY, APP_KEY_HEADER_NAME } from "#config/app.js"
|
||||
import app from "@adonisjs/core/services/app"
|
||||
import limiter from "@adonisjs/limiter/services/main"
|
||||
|
||||
export const throttle = limiter.define("global", () => {
|
||||
if (app.inTest) {
|
||||
export const throttle = limiter.define("global", (context) => {
|
||||
if (app.inTest || context.request.header(APP_KEY_HEADER_NAME) === APP_KEY) {
|
||||
return limiter.noLimit()
|
||||
}
|
||||
return limiter.allowRequests(120).every("1 minute")
|
||||
|
Reference in New Issue
Block a user