beavuck-time 2.3.6 → 2.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/api.js +20 -0
- package/build/config/corsOptions.js +60 -0
- package/build/config/logger.js +36 -0
- package/build/controllers/nowController.js +30 -0
- package/build/errors/BeavuckTimeClientError.js +13 -0
- package/build/errors/BeavuckTimeServerError.js +16 -0
- package/build/errors/CorsError.js +13 -0
- package/build/errors/base/BeavuckTimeError.js +14 -0
- package/build/middlewares/corsMiddleware.js +34 -0
- package/build/middlewares/errorHandler.js +47 -0
- package/build/middlewares/notFoundHandler.js +9 -0
- package/build/middlewares/rateLimiter.js +16 -0
- package/build/models/now.js +14 -0
- package/build/routes/routes.js +54 -0
- package/build/server.js +63 -0
- package/build/services/nowService.js +11 -0
- package/build/types/isoTimestamp.js +4 -0
- package/build/utils/urlUtil.js +31 -0
- package/openapi/swagger.json +1 -1
- package/package.json +13 -3
- package/.env +0 -21
- package/.env.test +0 -30
- package/.gitlab/issue_templates/bug.md +0 -53
- package/.gitlab/issue_templates/enhancement.md +0 -53
- package/.gitlab/merge_request_templates/bug_fix.md +0 -9
- package/.gitlab/merge_request_templates/enhancement.md +0 -9
- package/.gitlab-ci.yml +0 -158
- package/.nvmrc +0 -1
- package/.sonarlint/connectedMode.json +0 -4
- package/DOCKERHUB_OVERVIEW.md +0 -185
- package/Dockerfile +0 -25
- package/HELP.md +0 -53
- package/api-tests/bruno/bruno.json +0 -9
- package/api-tests/bruno/collection.bru +0 -7
- package/api-tests/bruno/environments/local.bru +0 -8
- package/api-tests/bruno/now/nok/nok_no_origin.bru +0 -19
- package/api-tests/bruno/now/nok/nok_non_recognized_referrer.bru +0 -23
- package/api-tests/bruno/now/nok/nok_non_trusted_origin.bru +0 -23
- package/api-tests/bruno/now/nok/nok_non_truted_origin_nor_referrer.bru +0 -24
- package/api-tests/bruno/now/ok/ok_same_origin.bru +0 -23
- package/api-tests/bruno/now/ok/ok_same_referrer.bru +0 -23
- package/api-tests/bruno/now/ok/ok_trusted_origin.bru +0 -23
- package/eslint.config.mjs +0 -28
- package/sonar-project.properties +0 -9
- package/src/__tests__/api.test.ts +0 -98
- package/src/__tests__/middlewares/errorHandler.test.ts +0 -93
- package/src/__tests__/utils/urlUtil.test.ts +0 -53
- package/src/api.ts +0 -19
- package/src/config/corsOptions.ts +0 -63
- package/src/config/logger.ts +0 -38
- package/src/controllers/nowController.ts +0 -19
- package/src/errors/BeavuckTimeClientError.ts +0 -12
- package/src/errors/BeavuckTimeServerError.ts +0 -18
- package/src/errors/CorsError.ts +0 -12
- package/src/errors/base/BeavuckTimeError.ts +0 -12
- package/src/middlewares/corsMiddleware.ts +0 -32
- package/src/middlewares/errorHandler.ts +0 -57
- package/src/middlewares/notFoundHandler.ts +0 -8
- package/src/middlewares/rateLimiter.ts +0 -15
- package/src/models/now.ts +0 -14
- package/src/routes/routes.ts +0 -84
- package/src/server.ts +0 -83
- package/src/services/nowService.ts +0 -9
- package/src/types/isoTimestamp.ts +0 -8
- package/src/utils/urlUtil.ts +0 -29
- package/tsconfig.json +0 -38
- package/tsoa.json +0 -12
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
meta {
|
|
2
|
-
name: ok_same_origin
|
|
3
|
-
type: http
|
|
4
|
-
seq: 2
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
get {
|
|
8
|
-
url: {{BASE_URL}}/now
|
|
9
|
-
body: none
|
|
10
|
-
auth: inherit
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
headers {
|
|
14
|
-
Origin: {{BASE_URL}}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
assert {
|
|
18
|
-
res.status: eq 200
|
|
19
|
-
res.body: isDefined
|
|
20
|
-
res.body.now: isDefined
|
|
21
|
-
res.body.now: matches {{RFC_3339_REGEX}}
|
|
22
|
-
res.responseTime: lt 200
|
|
23
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
meta {
|
|
2
|
-
name: ok_same_referrer
|
|
3
|
-
type: http
|
|
4
|
-
seq: 3
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
get {
|
|
8
|
-
url: {{BASE_URL}}/now
|
|
9
|
-
body: none
|
|
10
|
-
auth: inherit
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
headers {
|
|
14
|
-
Referrer: {{BASE_URL}}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
assert {
|
|
18
|
-
res.status: eq 200
|
|
19
|
-
res.body: isDefined
|
|
20
|
-
res.body.now: isDefined
|
|
21
|
-
res.body.now: matches {{RFC_3339_REGEX}}
|
|
22
|
-
res.responseTime: lt 200
|
|
23
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
meta {
|
|
2
|
-
name: ok_trusted_origin
|
|
3
|
-
type: http
|
|
4
|
-
seq: 1
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
get {
|
|
8
|
-
url: {{BASE_URL}}/now
|
|
9
|
-
body: none
|
|
10
|
-
auth: inherit
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
headers {
|
|
14
|
-
Origin: {{TRUSTED_ORIGIN}}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
assert {
|
|
18
|
-
res.status: eq 200
|
|
19
|
-
res.body: isDefined
|
|
20
|
-
res.body.now: isDefined
|
|
21
|
-
res.body.now: matches {{RFC_3339_REGEX}}
|
|
22
|
-
res.responseTime: lt 200
|
|
23
|
-
}
|
package/eslint.config.mjs
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// eslint.config.mjs
|
|
2
|
-
|
|
3
|
-
import globals from 'globals'
|
|
4
|
-
import pluginJs from '@eslint/js'
|
|
5
|
-
import tseslint from 'typescript-eslint'
|
|
6
|
-
|
|
7
|
-
/** @type {import('eslint').Linter.Config[]} */
|
|
8
|
-
export default [
|
|
9
|
-
{files: ['**/*.{js,mjs,cjs,ts}']},
|
|
10
|
-
{languageOptions: {globals: globals.browser}},
|
|
11
|
-
pluginJs.configs.recommended,
|
|
12
|
-
...tseslint.configs.recommended,
|
|
13
|
-
{
|
|
14
|
-
rules: {
|
|
15
|
-
'@typescript-eslint/no-explicit-any': 'off',
|
|
16
|
-
'no-restricted-syntax': [
|
|
17
|
-
'error', {
|
|
18
|
-
selector: 'CallExpression[callee.object.name="console"]',
|
|
19
|
-
message: 'Don\'t use console -- use the configured logger instead',
|
|
20
|
-
},
|
|
21
|
-
'error', {
|
|
22
|
-
selector: ':not(BinaryExpression:matches([operator="!=="], [operator="==="])) > Literal[value="null"]',
|
|
23
|
-
message: 'Usage of "null" is deprecated except when received from legacy APIs. Use "undefined" instead',
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
]
|
package/sonar-project.properties
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
sonar.projectKey=beavuck-services_time
|
|
2
|
-
sonar.organization=beavuck-services
|
|
3
|
-
sonar.sources=.
|
|
4
|
-
sonar.exclusions=**/node_modules/**, **/coverage/**, **/build/**, **/build/**, **/api-tests/**, *package*.json, src/routes/routes.ts
|
|
5
|
-
sonar.coverage.exclusions=src/__tests__/utils/urlUtil.test.ts, src/__tests__/middlewares/errorHandler.test.ts, src/__tests__/config/corsOptions.test.ts, src/__tests__/api.test.ts, src/config/logger.ts, src/server.ts, src/middlewares/rateLimiter.ts, src/routes/routes.ts
|
|
6
|
-
sonar.sourceEncoding=UTF-8
|
|
7
|
-
sonar.typescript.tsconfigPaths=tsconfig.json
|
|
8
|
-
sonar.coverage.inclusions=src/**/*.*
|
|
9
|
-
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import request from 'supertest'
|
|
2
|
-
import {api} from '../api'
|
|
3
|
-
import {StatusCodes} from 'http-status-codes'
|
|
4
|
-
import {RFC_3339_FORMAT} from '../types/isoTimestamp'
|
|
5
|
-
|
|
6
|
-
let callsMadeSoFar = 0
|
|
7
|
-
|
|
8
|
-
function getSomeTrustedOrigin(): string {
|
|
9
|
-
return process.env.BEAVUCK_TIME_TRUSTED_ORIGINS?.split(',')[0] ?? ''
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Increments the callsMadeSoFar counter and returns the response and trusted origin
|
|
14
|
-
*/
|
|
15
|
-
async function makeSuccessfulCallToNowEndpoint() {
|
|
16
|
-
const trustedOrigin = getSomeTrustedOrigin()
|
|
17
|
-
const res = await request(api).get('/now').set('Origin', trustedOrigin)
|
|
18
|
-
callsMadeSoFar++
|
|
19
|
-
return {res, trustedOrigin}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe('App Initialization', () => {
|
|
23
|
-
it('should disable x-powered-by header', async () => {
|
|
24
|
-
const res = await request(api).get('/now')
|
|
25
|
-
expect(res.header['x-powered-by']).toBeUndefined()
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('should allow requests from trusted origins', async () => {
|
|
29
|
-
const {res, trustedOrigin} = await makeSuccessfulCallToNowEndpoint()
|
|
30
|
-
expect(res.header['access-control-allow-origin']).toBe(trustedOrigin)
|
|
31
|
-
expect(res.status).toBe(StatusCodes.OK)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('should block requests from non-trusted origins', async () => {
|
|
35
|
-
const res = await request(api).get('/now').set('Origin', 'https://non-trusted.com')
|
|
36
|
-
expect(res.header['access-control-allow-origin']).toBeUndefined()
|
|
37
|
-
expect(res.status).toBe(StatusCodes.FORBIDDEN)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('should block requests with no origin header', async () => {
|
|
41
|
-
const res = await request(api).get('/now')
|
|
42
|
-
expect(res.header['access-control-allow-origin']).toBeUndefined()
|
|
43
|
-
expect(res.status).toBe(StatusCodes.FORBIDDEN)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('should allow requests with no origin header but a referrer header identical to server url', async () => {
|
|
47
|
-
const REFERRER: string = process.env.BEAVUCK_TIME_HOST_URL!
|
|
48
|
-
const res = await request(api).get('/now').set('Referrer', REFERRER)
|
|
49
|
-
expect(res.header['access-control-allow-origin']).toBeUndefined()
|
|
50
|
-
expect(res.status).toBe(StatusCodes.OK)
|
|
51
|
-
callsMadeSoFar++
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('should allow requests with no origin header but a referer (sic) header identical to server url', async () => {
|
|
55
|
-
const REFERER: string = process.env.BEAVUCK_TIME_HOST_URL!
|
|
56
|
-
const res = await request(api).get('/now').set('Referer', REFERER)
|
|
57
|
-
expect(res.header['access-control-allow-origin']).toBeUndefined()
|
|
58
|
-
expect(res.status).toBe(StatusCodes.OK)
|
|
59
|
-
callsMadeSoFar++
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('should handle errors using errorHandler middleware', async () => {
|
|
63
|
-
const res = await request(api).get('/nope').set('Origin', getSomeTrustedOrigin())
|
|
64
|
-
expect(res.status).toBe(StatusCodes.NOT_FOUND)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('should return the current time in ISO format', async () => {
|
|
68
|
-
const {res} = await makeSuccessfulCallToNowEndpoint()
|
|
69
|
-
expect(res.status).toBe(StatusCodes.OK)
|
|
70
|
-
expect(res.body).toHaveProperty('now')
|
|
71
|
-
expect(res.body.now).toMatch(RFC_3339_FORMAT)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('should return 500 if HOST_URL is not set', async () => {
|
|
75
|
-
const originalHostUrl = process.env.BEAVUCK_TIME_HOST_URL
|
|
76
|
-
delete process.env.BEAVUCK_TIME_HOST_URL
|
|
77
|
-
|
|
78
|
-
const res = await request(api).get('/now')
|
|
79
|
-
expect(res.status).toBe(StatusCodes.INTERNAL_SERVER_ERROR)
|
|
80
|
-
|
|
81
|
-
process.env.BEAVUCK_TIME_HOST_URL = originalHostUrl
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('should block requests with non-trusted referrer header if origin is not OK', async () => {
|
|
85
|
-
const res = await request(api).get('/now').set('Referrer', 'https://non-trusted.com')
|
|
86
|
-
expect(res.status).toBe(StatusCodes.FORBIDDEN)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('should block requests that exceed the rate limit', async () => {
|
|
90
|
-
const RATE_LIMIT = parseInt(process.env.BEAVUCK_TIME_RATE_LIMIT!, 10)
|
|
91
|
-
while (callsMadeSoFar <= RATE_LIMIT) {
|
|
92
|
-
const {res} = await makeSuccessfulCallToNowEndpoint()
|
|
93
|
-
if (callsMadeSoFar > RATE_LIMIT) {
|
|
94
|
-
expect(res.status).toBe(StatusCodes.TOO_MANY_REQUESTS)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
})
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
// src/__tests__/middlewares/errorHandler.test.ts
|
|
2
|
-
|
|
3
|
-
import express from 'express'
|
|
4
|
-
import {errorHandler} from '../../middlewares/errorHandler'
|
|
5
|
-
import {BeavuckTimeClientError} from '../../errors/BeavuckTimeClientError'
|
|
6
|
-
import {BeavuckTimeServerError} from '../../errors/BeavuckTimeServerError'
|
|
7
|
-
import {CorsError} from '../../errors/CorsError'
|
|
8
|
-
import {StatusCodes} from 'http-status-codes'
|
|
9
|
-
import {logger} from '../../config/logger'
|
|
10
|
-
import {ValidateError} from 'tsoa'
|
|
11
|
-
|
|
12
|
-
jest.mock('../../config/logger')
|
|
13
|
-
|
|
14
|
-
describe('errorHandler', () => {
|
|
15
|
-
let req: express.Request
|
|
16
|
-
let res: express.Response
|
|
17
|
-
let next: express.NextFunction
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
req = {} as express.Request
|
|
21
|
-
res = {
|
|
22
|
-
status: jest.fn().mockReturnThis(),
|
|
23
|
-
json: jest.fn().mockReturnThis(),
|
|
24
|
-
} as unknown as express.Response
|
|
25
|
-
next = jest.fn() as express.NextFunction
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('should handle BeavuckTimeClientError', () => {
|
|
29
|
-
const err = new BeavuckTimeClientError('Some message')
|
|
30
|
-
errorHandler(err, req, res, next)
|
|
31
|
-
expect(logger.warn).toHaveBeenCalledWith(err)
|
|
32
|
-
expect(res.status).toHaveBeenCalledWith(err.code)
|
|
33
|
-
expect(res.json).toHaveBeenCalledWith({message: `${err.message}`})
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('should handle CorsError', () => {
|
|
37
|
-
const err = new CorsError('Some message')
|
|
38
|
-
errorHandler(err, req, res, next)
|
|
39
|
-
expect(logger.warn).toHaveBeenCalledWith(err)
|
|
40
|
-
expect(res.status).toHaveBeenCalledWith(err.code)
|
|
41
|
-
expect(res.json).toHaveBeenCalledWith({message: `${err.message}`})
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('should handle BeavuckTimeServerError', () => {
|
|
45
|
-
const err = new BeavuckTimeServerError('Some message')
|
|
46
|
-
errorHandler(err, req, res, next)
|
|
47
|
-
expect(logger.error).toHaveBeenCalledWith(err)
|
|
48
|
-
expect(res.status).toHaveBeenCalledWith(err.code)
|
|
49
|
-
expect(res.json).toHaveBeenCalledWith({message: `${err.message}`})
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('should handle non-specific error', () => {
|
|
53
|
-
const err = new Error('Some message')
|
|
54
|
-
errorHandler(err, req, res, next)
|
|
55
|
-
const beavuckError = BeavuckTimeServerError.fromError(err)
|
|
56
|
-
expect(logger.error).toHaveBeenCalledWith(beavuckError)
|
|
57
|
-
expect(res.status).toHaveBeenCalledWith(StatusCodes.INTERNAL_SERVER_ERROR)
|
|
58
|
-
expect(res.json).toHaveBeenCalledWith({
|
|
59
|
-
message: `${beavuckError.message}`,
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('should call next if no error', () => {
|
|
64
|
-
errorHandler(undefined, req, res, next)
|
|
65
|
-
expect(next).toHaveBeenCalled()
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('should handle ValidateError', () => {
|
|
69
|
-
const err = new ValidateError({}, 'Some message')
|
|
70
|
-
errorHandler(err, req, res, next)
|
|
71
|
-
expect(logger.error).toHaveBeenCalled()
|
|
72
|
-
expect(res.status).toHaveBeenCalledWith(StatusCodes.UNPROCESSABLE_ENTITY)
|
|
73
|
-
expect(res.json).toHaveBeenCalledWith({
|
|
74
|
-
message: 'Validation Error',
|
|
75
|
-
details: err.fields,
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('should handle unknown error type', () => {
|
|
80
|
-
const err = {some: 'unknown error'}
|
|
81
|
-
errorHandler(err, req, res, next)
|
|
82
|
-
expect(logger.error).toHaveBeenCalled()
|
|
83
|
-
expect(res.status).toHaveBeenCalledWith(StatusCodes.INTERNAL_SERVER_ERROR)
|
|
84
|
-
expect(res.json).toHaveBeenCalledWith({
|
|
85
|
-
message: 'Internal Server Error',
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('should handle null error', () => {
|
|
90
|
-
errorHandler(undefined, req, res, next)
|
|
91
|
-
expect(next).toHaveBeenCalled()
|
|
92
|
-
})
|
|
93
|
-
})
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
// src/__tests__/utils/urlUtil.test.ts
|
|
2
|
-
|
|
3
|
-
import {isSameOrigin, tryParseUrl} from '../../utils/urlUtil'
|
|
4
|
-
import {URL} from 'url'
|
|
5
|
-
|
|
6
|
-
const A_VALID_URL_STRING = 'https://example.com:1234'
|
|
7
|
-
const SAME_VALID_URL_DIFFERENT_PORT = 'https://example.com:81'
|
|
8
|
-
const SOME_OTHER_VALID_URL_STRING = 'https://different.com:1234'
|
|
9
|
-
const AN_INVALID_URL_STRING = 'invalid'
|
|
10
|
-
|
|
11
|
-
describe('urlUtil', () => {
|
|
12
|
-
describe('isSameOrigin', () => {
|
|
13
|
-
it('returns false when either URL is undefined', () => {
|
|
14
|
-
const url = new URL(A_VALID_URL_STRING)
|
|
15
|
-
expect(isSameOrigin(url)).toBe(false)
|
|
16
|
-
expect(isSameOrigin(undefined, url)).toBe(false)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('returns true when URLs have the same origin', () => {
|
|
20
|
-
const url1 = new URL(A_VALID_URL_STRING)
|
|
21
|
-
const url2 = new URL(A_VALID_URL_STRING)
|
|
22
|
-
expect(isSameOrigin(url1, url2)).toBe(true)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('returns false when URLs have the same origin but different ports', () => {
|
|
26
|
-
const url1 = new URL(A_VALID_URL_STRING)
|
|
27
|
-
const url2 = new URL(SAME_VALID_URL_DIFFERENT_PORT)
|
|
28
|
-
expect(isSameOrigin(url1, url2)).toBe(false)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('returns false when URLs have different origins', () => {
|
|
32
|
-
const url1 = new URL(A_VALID_URL_STRING)
|
|
33
|
-
const url2 = new URL(SOME_OTHER_VALID_URL_STRING)
|
|
34
|
-
expect(isSameOrigin(url1, url2)).toBe(false)
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
describe('tryParseUrl', () => {
|
|
39
|
-
it('returns undefined when URL string is undefined', () => {
|
|
40
|
-
expect(tryParseUrl()).toBeUndefined()
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('returns a URL object when URL string is valid', () => {
|
|
44
|
-
const url = tryParseUrl(A_VALID_URL_STRING)
|
|
45
|
-
expect(url).toBeInstanceOf(URL)
|
|
46
|
-
expect(url?.origin).toBe(A_VALID_URL_STRING)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('returns undefined when URL string is invalid', () => {
|
|
50
|
-
expect(tryParseUrl(AN_INVALID_URL_STRING)).toBeUndefined()
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
})
|
package/src/api.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// src/api.ts
|
|
2
|
-
|
|
3
|
-
import express from 'express'
|
|
4
|
-
import {corsMiddleware} from './middlewares/corsMiddleware'
|
|
5
|
-
import {errorHandler} from './middlewares/errorHandler'
|
|
6
|
-
import {rateLimiter} from './middlewares/rateLimiter'
|
|
7
|
-
import {notFoundHandler} from './middlewares/notFoundHandler'
|
|
8
|
-
import {RegisterRoutes} from './routes/routes'
|
|
9
|
-
|
|
10
|
-
export const api = express()
|
|
11
|
-
api.disable('x-powered-by')
|
|
12
|
-
|
|
13
|
-
api.use(corsMiddleware)
|
|
14
|
-
api.use(errorHandler)
|
|
15
|
-
api.use(rateLimiter)
|
|
16
|
-
|
|
17
|
-
RegisterRoutes(api)
|
|
18
|
-
|
|
19
|
-
api.use(notFoundHandler)
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// src/config/corsOptions.ts
|
|
2
|
-
|
|
3
|
-
import {CorsOptions} from 'cors'
|
|
4
|
-
import {CorsError} from '../errors/CorsError'
|
|
5
|
-
import {isSameOrigin, tryParseUrl} from '../utils/urlUtil'
|
|
6
|
-
import {StatusCodes} from 'http-status-codes'
|
|
7
|
-
import dotenvx from '@dotenvx/dotenvx'
|
|
8
|
-
|
|
9
|
-
dotenvx.config()
|
|
10
|
-
|
|
11
|
-
export const corsOptions: CorsOptions = {
|
|
12
|
-
methods: ['GET', 'OPTIONS'],
|
|
13
|
-
optionsSuccessStatus: StatusCodes.OK,
|
|
14
|
-
origin: (
|
|
15
|
-
origin: string | undefined,
|
|
16
|
-
callback: (err: Error | null, allow?: boolean) => void,
|
|
17
|
-
): void => {
|
|
18
|
-
if (
|
|
19
|
-
isAllTrusted() ||
|
|
20
|
-
isSameOrigin(getHostUrl(), tryParseUrl(origin)) ||
|
|
21
|
-
isOriginAbsentOrTrusted(origin)
|
|
22
|
-
) {
|
|
23
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
24
|
-
callback(null, true)
|
|
25
|
-
} else {
|
|
26
|
-
callback(new CorsError(origin), false)
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let HOST_URL: URL
|
|
32
|
-
let TRUSTED_ORIGINS: string[]
|
|
33
|
-
|
|
34
|
-
export function getHostUrl(): URL | undefined {
|
|
35
|
-
if (!HOST_URL) {
|
|
36
|
-
initHostUrl()
|
|
37
|
-
}
|
|
38
|
-
return HOST_URL
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function initHostUrl(): void {
|
|
42
|
-
HOST_URL = new URL(process.env.BEAVUCK_TIME_HOST_URL!)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function getTrustedOrigins(): string[] {
|
|
46
|
-
if (!TRUSTED_ORIGINS || TRUSTED_ORIGINS.length === 0) {
|
|
47
|
-
initTrustedOrigins()
|
|
48
|
-
}
|
|
49
|
-
return TRUSTED_ORIGINS
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function initTrustedOrigins(): void {
|
|
53
|
-
const trustedOrigins: string = process.env.BEAVUCK_TIME_TRUSTED_ORIGINS!
|
|
54
|
-
TRUSTED_ORIGINS = trustedOrigins.split(',')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function isAllTrusted(): boolean {
|
|
58
|
-
return getTrustedOrigins().includes('*')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function isOriginAbsentOrTrusted(origin?: string): boolean {
|
|
62
|
-
return !origin || getTrustedOrigins().includes(origin)
|
|
63
|
-
}
|
package/src/config/logger.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// src/config/logger.ts
|
|
2
|
-
|
|
3
|
-
import {createLogger, format, transports} from 'winston'
|
|
4
|
-
import 'winston-daily-rotate-file'
|
|
5
|
-
import dotenvx from '@dotenvx/dotenvx'
|
|
6
|
-
|
|
7
|
-
dotenvx.config()
|
|
8
|
-
|
|
9
|
-
const LOGS_DIR = 'logs'
|
|
10
|
-
const LOG_LEVEL: string = process.env.BEAVUCK_TIME_LOG_LEVEL ?? 'info'
|
|
11
|
-
const MAX_LOG_FILES: string | number = process.env.BEAVUCK_TIME_MAX_LOG_FILES ?? 64
|
|
12
|
-
const MAX_SIZE_LOG_FILES: string = process.env.BEAVUCK_TIME_MAX_SIZE_LOG_FILES ?? '1m'
|
|
13
|
-
|
|
14
|
-
const dailyRotateFileTransport = new transports.DailyRotateFile({
|
|
15
|
-
filename: `${LOGS_DIR}/%DATE%-combined.log`,
|
|
16
|
-
datePattern: 'YYYY-MM-DD',
|
|
17
|
-
zippedArchive: true,
|
|
18
|
-
maxSize: MAX_SIZE_LOG_FILES,
|
|
19
|
-
maxFiles: MAX_LOG_FILES,
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
export const logger = createLogger({
|
|
23
|
-
level: LOG_LEVEL,
|
|
24
|
-
format: format.combine(
|
|
25
|
-
format.timestamp(),
|
|
26
|
-
format.printf(({timestamp, level, message}) => {
|
|
27
|
-
return `${timestamp} [${level.toUpperCase()}]: ${message}`
|
|
28
|
-
}),
|
|
29
|
-
),
|
|
30
|
-
transports: [
|
|
31
|
-
new transports.Console(),
|
|
32
|
-
new transports.File({
|
|
33
|
-
filename: `${LOGS_DIR}/error.log`,
|
|
34
|
-
level: 'error',
|
|
35
|
-
}),
|
|
36
|
-
dailyRotateFileTransport,
|
|
37
|
-
],
|
|
38
|
-
})
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// src/controllers/nowController.ts
|
|
2
|
-
|
|
3
|
-
import {Controller, Get, OperationId, Route, Tags} from 'tsoa'
|
|
4
|
-
import {Now} from '../models/now'
|
|
5
|
-
import {NowService} from '../services/nowService'
|
|
6
|
-
|
|
7
|
-
@Route('now')
|
|
8
|
-
export class NowController extends Controller {
|
|
9
|
-
/**
|
|
10
|
-
* Get the current time in ISO format, in UTC timezone
|
|
11
|
-
* @summary Get current time
|
|
12
|
-
*/
|
|
13
|
-
@Tags('now')
|
|
14
|
-
@Get()
|
|
15
|
-
@OperationId('getNow')
|
|
16
|
-
public async getNow(): Promise<Now> {
|
|
17
|
-
return new NowService().get()
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// src/errors/BeavuckTimeClientError.ts
|
|
2
|
-
|
|
3
|
-
import {BeavuckTimeError} from './base/BeavuckTimeError'
|
|
4
|
-
import {StatusCodes} from 'http-status-codes'
|
|
5
|
-
|
|
6
|
-
export class BeavuckTimeClientError extends BeavuckTimeError {
|
|
7
|
-
static readonly baseMessage: string = 'Client Error'
|
|
8
|
-
|
|
9
|
-
constructor(message: string, code: number = StatusCodes.BAD_REQUEST) {
|
|
10
|
-
super(`${BeavuckTimeClientError.baseMessage}: ${message}`, code)
|
|
11
|
-
}
|
|
12
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// src/errors/BeavuckTimeServerError.ts
|
|
2
|
-
|
|
3
|
-
import {BeavuckTimeError} from './base/BeavuckTimeError'
|
|
4
|
-
import {StatusCodes} from 'http-status-codes'
|
|
5
|
-
|
|
6
|
-
export class BeavuckTimeServerError extends BeavuckTimeError {
|
|
7
|
-
static readonly baseMessage: string = 'Internal Server Error'
|
|
8
|
-
|
|
9
|
-
constructor(message: string, code: number = StatusCodes.INTERNAL_SERVER_ERROR) {
|
|
10
|
-
super(`${BeavuckTimeServerError.baseMessage}: ${message}`, code)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
static fromError(error: Error): BeavuckTimeServerError {
|
|
14
|
-
return error instanceof BeavuckTimeServerError ? error : (
|
|
15
|
-
new BeavuckTimeServerError(error.message)
|
|
16
|
-
)
|
|
17
|
-
}
|
|
18
|
-
}
|
package/src/errors/CorsError.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// src/errors/CorsError.ts
|
|
2
|
-
|
|
3
|
-
import {StatusCodes} from 'http-status-codes'
|
|
4
|
-
import {BeavuckTimeClientError} from './BeavuckTimeClientError'
|
|
5
|
-
|
|
6
|
-
export class CorsError extends BeavuckTimeClientError {
|
|
7
|
-
static readonly baseMessage: string = 'Origin forbidden by CORS'
|
|
8
|
-
|
|
9
|
-
constructor(origin?: string) {
|
|
10
|
-
super(`${CorsError.baseMessage}: ${origin}`, StatusCodes.FORBIDDEN)
|
|
11
|
-
}
|
|
12
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// src/errors/base/BeavuckTimeError.ts
|
|
2
|
-
|
|
3
|
-
export abstract class BeavuckTimeError extends Error {
|
|
4
|
-
code: number
|
|
5
|
-
|
|
6
|
-
protected constructor(message: string, code: number) {
|
|
7
|
-
super(message)
|
|
8
|
-
this.name = this.constructor.name
|
|
9
|
-
this.code = code
|
|
10
|
-
Error.captureStackTrace(this, this.constructor)
|
|
11
|
-
}
|
|
12
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// src/middlewares/corsMiddleware.ts
|
|
2
|
-
|
|
3
|
-
import cors from 'cors'
|
|
4
|
-
import {isSameOrigin, tryParseUrl} from '../utils/urlUtil'
|
|
5
|
-
import {CorsError} from '../errors/CorsError'
|
|
6
|
-
import express from 'express'
|
|
7
|
-
import {logger} from '../config/logger'
|
|
8
|
-
import {BeavuckTimeServerError} from '../errors/BeavuckTimeServerError'
|
|
9
|
-
import {corsOptions} from '../config/corsOptions'
|
|
10
|
-
|
|
11
|
-
export const corsMiddleware = (
|
|
12
|
-
req: express.Request,
|
|
13
|
-
res: express.Response,
|
|
14
|
-
next: express.NextFunction,
|
|
15
|
-
) => {
|
|
16
|
-
const HOST_URL = tryParseUrl(process.env.BEAVUCK_TIME_HOST_URL ?? '')
|
|
17
|
-
// 'referer' is a misspelling that was kept for compatibility: https://en.wikipedia.org/wiki/HTTP_referer
|
|
18
|
-
const referrerHeader: string | undefined =
|
|
19
|
-
(req.headers.referrer as string) || req.headers.referer
|
|
20
|
-
if (req.headers.origin) {
|
|
21
|
-
logger.debug(`CORS request from ${req.headers.origin}`)
|
|
22
|
-
cors(corsOptions)(req, res, next)
|
|
23
|
-
} else if (!HOST_URL) {
|
|
24
|
-
const noHostUrl = 'Host URL not set'
|
|
25
|
-
next(new BeavuckTimeServerError(noHostUrl))
|
|
26
|
-
} else if (isSameOrigin(HOST_URL, tryParseUrl(referrerHeader ?? ''))) {
|
|
27
|
-
next()
|
|
28
|
-
} else {
|
|
29
|
-
const badOriginAndOrReferrer = `origin: ${req.headers.origin}, referrer: ${referrerHeader}`
|
|
30
|
-
next(new CorsError(badOriginAndOrReferrer))
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// src/middlewares/errorHandler.ts
|
|
2
|
-
|
|
3
|
-
import express from 'express'
|
|
4
|
-
import {logger} from '../config/logger'
|
|
5
|
-
import {BeavuckTimeClientError} from '../errors/BeavuckTimeClientError'
|
|
6
|
-
import {BeavuckTimeServerError} from '../errors/BeavuckTimeServerError'
|
|
7
|
-
import {StatusCodes} from 'http-status-codes'
|
|
8
|
-
import {ValidateError} from 'tsoa'
|
|
9
|
-
import {BeavuckTimeError} from '../errors/base/BeavuckTimeError'
|
|
10
|
-
|
|
11
|
-
export const errorHandler = (
|
|
12
|
-
err: unknown,
|
|
13
|
-
req: express.Request,
|
|
14
|
-
res: express.Response,
|
|
15
|
-
next: express.NextFunction,
|
|
16
|
-
) => {
|
|
17
|
-
if (err instanceof ValidateError) {
|
|
18
|
-
const logMsg = `Validation error on ${req.path}: ${err.fields}`
|
|
19
|
-
const resStatus = StatusCodes.UNPROCESSABLE_ENTITY
|
|
20
|
-
const resMsgObj = {message: 'Validation Error', details: err.fields}
|
|
21
|
-
handleOtherError(res, logMsg, resStatus, resMsgObj)
|
|
22
|
-
} else if (err instanceof BeavuckTimeClientError) {
|
|
23
|
-
handleBeavuckClientError(res, err)
|
|
24
|
-
} else if (err instanceof BeavuckTimeServerError || err instanceof Error) {
|
|
25
|
-
handleBeavuckServerError(res, BeavuckTimeServerError.fromError(err))
|
|
26
|
-
} else if (err) {
|
|
27
|
-
handleOtherError(res)
|
|
28
|
-
} else {
|
|
29
|
-
next()
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function sendErrorResponse(res: express.Response, err: BeavuckTimeError) {
|
|
34
|
-
res.status(err.code).json({message: err.message})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function handleBeavuckClientError(res: express.Response, err: BeavuckTimeClientError) {
|
|
38
|
-
logger.warn(err)
|
|
39
|
-
sendErrorResponse(res, err)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function handleBeavuckServerError(res: express.Response, err: BeavuckTimeServerError) {
|
|
43
|
-
logger.error(err)
|
|
44
|
-
sendErrorResponse(res, err)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function handleOtherError(
|
|
48
|
-
res: express.Response,
|
|
49
|
-
logMsg: string = `Unknown error`,
|
|
50
|
-
resStatus: StatusCodes = StatusCodes.INTERNAL_SERVER_ERROR,
|
|
51
|
-
resMsg: {message: string; details?: unknown} = {
|
|
52
|
-
message: BeavuckTimeServerError.baseMessage,
|
|
53
|
-
},
|
|
54
|
-
) {
|
|
55
|
-
logger.error(`${logMsg}: ${JSON.stringify(resMsg)}`)
|
|
56
|
-
res.status(resStatus).json(resMsg)
|
|
57
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// src/middlewares/notFoundHandler.ts
|
|
2
|
-
|
|
3
|
-
import {Request as ExpressRequest, Response as ExpressResponse} from 'express'
|
|
4
|
-
import {StatusCodes} from 'http-status-codes'
|
|
5
|
-
|
|
6
|
-
export const notFoundHandler = (_req: ExpressRequest, res: ExpressResponse) => {
|
|
7
|
-
res.status(StatusCodes.NOT_FOUND).send({message: 'Not Found'})
|
|
8
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// src/middlewares/rateLimiter.ts
|
|
2
|
-
|
|
3
|
-
import rateLimit from 'express-rate-limit'
|
|
4
|
-
|
|
5
|
-
const NO_RATE_LIMIT = '-1'
|
|
6
|
-
const RATE_LIMIT = parseInt(process.env.BEAVUCK_TIME_RATE_LIMIT ?? NO_RATE_LIMIT, 10)
|
|
7
|
-
|
|
8
|
-
const SECONDS_IN_ONE_MINUTE = 60
|
|
9
|
-
const MILLISECONDS_IN_ONE_SECOND = 1000
|
|
10
|
-
const ONE_MINUTE = SECONDS_IN_ONE_MINUTE * MILLISECONDS_IN_ONE_SECOND
|
|
11
|
-
|
|
12
|
-
export const rateLimiter =
|
|
13
|
-
RATE_LIMIT > 0 ?
|
|
14
|
-
rateLimit({windowMs: ONE_MINUTE, limit: RATE_LIMIT})
|
|
15
|
-
: (_req: any, _res: any, next: any) => next()
|