@samanbayaka/core 0.0.1
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/README.md +812 -0
- package/commit-hash.mjs +1 -0
- package/helper/mol-built-in/AjvValidator.mjs +90 -0
- package/helper/mol-built-in/CustomLogger.mjs +83 -0
- package/helper/mol-built-in/HybridCacher.mjs +155 -0
- package/helper/utility/access-token-validator.mjs +249 -0
- package/helper/utility/aux-broker-params-validator.mjs +126 -0
- package/helper/utility/check-syntax.mjs +91 -0
- package/helper/utility/config-handler.mjs +161 -0
- package/helper/utility/error-handler.mjs +456 -0
- package/helper/utility/file-handler.mjs +121 -0
- package/helper/utility/global-configs-validator.mjs +47 -0
- package/helper/utility/openapi-to-mol-params.mjs +84 -0
- package/helper/utility/sign-jwt.mjs +35 -0
- package/helper/utility/telemetry.mjs +129 -0
- package/index.mjs +427 -0
- package/package.json +78 -0
package/commit-hash.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const COMMIT_HASH = '125c4ea';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*AjvValidator.mjs*/
|
|
2
|
+
|
|
3
|
+
import Ajv from "ajv"
|
|
4
|
+
import addFormats from "ajv-formats"
|
|
5
|
+
import { Validators, Errors } from "moleculer"
|
|
6
|
+
|
|
7
|
+
const ajv = new Ajv({
|
|
8
|
+
allErrors: true,
|
|
9
|
+
strict: false,
|
|
10
|
+
coerceTypes: true //true auto fit data to integer data type like 12 if supplied "12"
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
addFormats(ajv)
|
|
14
|
+
|
|
15
|
+
export class AjvValidator extends Validators.Base {
|
|
16
|
+
constructor() {
|
|
17
|
+
super()
|
|
18
|
+
this.ajv = ajv
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
compile(schema) {
|
|
22
|
+
const validate = this.ajv.compile(schema)
|
|
23
|
+
|
|
24
|
+
return (params) => {
|
|
25
|
+
const valid = validate(params)
|
|
26
|
+
|
|
27
|
+
if (!valid) {
|
|
28
|
+
return formatAjvErrors(validate.errors, params)
|
|
29
|
+
}
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
export const validateOAS = (params, schema) => {
|
|
37
|
+
const validate = ajv.compile(schema)
|
|
38
|
+
const valid = validate(params)
|
|
39
|
+
if (!valid) {
|
|
40
|
+
throw new Errors.ValidationError(
|
|
41
|
+
"Validation failed",
|
|
42
|
+
"VALIDATION_ERROR",
|
|
43
|
+
formatAjvErrors(validate.errors, params)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return valid
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format AJV Errors
|
|
52
|
+
* @param {error object}
|
|
53
|
+
* @param {params object}
|
|
54
|
+
* @return {array}
|
|
55
|
+
*/
|
|
56
|
+
const formatAjvErrors = (errors, params) => {
|
|
57
|
+
if(!errors){
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return errors.map(err => {
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Remove leading "/" and subsequent "/" to "."
|
|
65
|
+
* @type {string}
|
|
66
|
+
*/
|
|
67
|
+
const path = err.instancePath
|
|
68
|
+
.replace(/^\/+/, '')
|
|
69
|
+
.replace(/\//g, '.')
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Getting value from nested object
|
|
73
|
+
* @param {string} like query.name.lastName
|
|
74
|
+
* @return {string}
|
|
75
|
+
*/
|
|
76
|
+
const val = path.split('.').reduce((acc, key) => acc?.[key], params)
|
|
77
|
+
|
|
78
|
+
if (err.keyword === "required") {
|
|
79
|
+
return {
|
|
80
|
+
path,
|
|
81
|
+
message: `Must have required property '${err.params.missingProperty}' in the path object.`
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
path,
|
|
86
|
+
message: `The value '${val}' ${err.message}`
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import pino from "pino"
|
|
2
|
+
|
|
3
|
+
const pinoOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* ISO timestamps
|
|
6
|
+
*/
|
|
7
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* base fields = Loki labels candidates
|
|
11
|
+
*/
|
|
12
|
+
base: null,
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rename default fields (optional but cleaner for Loki)
|
|
16
|
+
* @type {Object}
|
|
17
|
+
*/
|
|
18
|
+
// formatters: {
|
|
19
|
+
// level(label) {
|
|
20
|
+
// return { level: label } // instead of numeric
|
|
21
|
+
// }
|
|
22
|
+
// },
|
|
23
|
+
|
|
24
|
+
serializers: {
|
|
25
|
+
err: pino.stdSerializers.err
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// messageKey: "message",
|
|
29
|
+
nestedKey: 'data',
|
|
30
|
+
// msgPrefix: " ❖❖ SBK ❖❖ ",
|
|
31
|
+
hooks: {
|
|
32
|
+
logMethod(args, method, level) {
|
|
33
|
+
let mergedObj = {tag: "MOL", msgHdr: " ━━━━━━━❖❖ SBK ❖❖━━━━━━━ ", message: ""}
|
|
34
|
+
let messages = []
|
|
35
|
+
let tag
|
|
36
|
+
|
|
37
|
+
for (const arg of args) {
|
|
38
|
+
if (typeof arg === 'string') {
|
|
39
|
+
messages.push(arg.replace(/\u001b\[[0-9;]*m/g, " "))
|
|
40
|
+
}
|
|
41
|
+
else if (arg && typeof arg === 'object') {
|
|
42
|
+
if( arg.message ){
|
|
43
|
+
arg.message = typeof arg.message === 'object'
|
|
44
|
+
? JSON.stringify(arg.message)
|
|
45
|
+
: arg.message
|
|
46
|
+
messages.push(arg.message)
|
|
47
|
+
}
|
|
48
|
+
mergedObj = { ...mergedObj, ...arg }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Delete message header when tag is not "SBK"
|
|
54
|
+
*/
|
|
55
|
+
if( !(mergedObj?.tag == "SBK") ){
|
|
56
|
+
delete mergedObj.msgHdr
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Rebuild args into standard Pino format
|
|
61
|
+
*/
|
|
62
|
+
args = [{...mergedObj, message: messages.join(' ')}]
|
|
63
|
+
|
|
64
|
+
method.apply(this, args)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
export default (opts) => {
|
|
71
|
+
const { logger, logLevel } = opts
|
|
72
|
+
return logger == "CustomLogger"
|
|
73
|
+
? {
|
|
74
|
+
type: "Pino",
|
|
75
|
+
options: {
|
|
76
|
+
level: (logLevel || "info").toLowerCase(),
|
|
77
|
+
pino: {
|
|
78
|
+
options: pinoOptions
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
: "Console"
|
|
83
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Cachers } from "moleculer"
|
|
2
|
+
|
|
3
|
+
const MAX_L1_TTL = 600 // 10 minutes in seconds
|
|
4
|
+
const MAX_L2_TTL = 8640_000 // 100 days in seconds
|
|
5
|
+
const MAX_DATA_SZ = 10*1024*1024 // maximum data size in byte
|
|
6
|
+
|
|
7
|
+
export default class HybridCacher extends Cachers.Base {
|
|
8
|
+
constructor(opts = {}) {
|
|
9
|
+
super(opts)
|
|
10
|
+
|
|
11
|
+
this.memory = new Cachers.MemoryLRU({
|
|
12
|
+
max: opts.max || 1000,
|
|
13
|
+
ttl: opts.ttl[1]
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
this.redis = new Cachers.Redis({
|
|
17
|
+
ttl: opts.ttl[0],
|
|
18
|
+
redis: opts.redis
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async init(broker) {
|
|
23
|
+
this.logger = broker.logger
|
|
24
|
+
await this.memory.init(broker)
|
|
25
|
+
await this.redis.init(broker)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Helper to extract key prifix used by redis
|
|
31
|
+
* @param {string} key
|
|
32
|
+
* @return {string}
|
|
33
|
+
*/
|
|
34
|
+
getRedisCatchKey(key) {
|
|
35
|
+
return this.redis.prefix ? this.redis.prefix + key : key
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async get(key) {
|
|
39
|
+
const prefixedKey = this.getRedisCatchKey(key)
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Try L1 first
|
|
43
|
+
* @type {object | string}
|
|
44
|
+
*/
|
|
45
|
+
let data = await this.memory.get(prefixedKey)
|
|
46
|
+
if (data) {
|
|
47
|
+
this.logger.debug({tag: "SBK", message: 'L1 Hits'})
|
|
48
|
+
return data
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fallback to Redis
|
|
53
|
+
* @type {object | string}
|
|
54
|
+
*/
|
|
55
|
+
data = await this.redis.get(key)
|
|
56
|
+
if (data) {
|
|
57
|
+
/**
|
|
58
|
+
* Warm L1 using the TTL of L1 and the remaining time to L2, whichever is lower.
|
|
59
|
+
*/
|
|
60
|
+
const l1WarmTTL = Math.min((data.ttl[0] - ( Date.now() - data.txnTs)/1000), data.ttl[1])
|
|
61
|
+
this.logger.debug({tag: "SBK", message: 'L2 Hits'})
|
|
62
|
+
if( l1WarmTTL > 0 ) {
|
|
63
|
+
await this.memory.set(prefixedKey, data.result, l1WarmTTL)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return data?.result
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async set(key, data, ttl, meta = {}) {
|
|
71
|
+
const prefixedKey = this.getRedisCatchKey(key)
|
|
72
|
+
|
|
73
|
+
const dataSize = ((szData = this.redis.serializer.serialize(data)) => {
|
|
74
|
+
return Buffer.isBuffer(szData)
|
|
75
|
+
? szData.length
|
|
76
|
+
: Buffer.byteLength(szData)
|
|
77
|
+
})()
|
|
78
|
+
|
|
79
|
+
if(dataSize > MAX_DATA_SZ){
|
|
80
|
+
this.logger.error({
|
|
81
|
+
tag: "SBK",
|
|
82
|
+
message: `Cache data size is '${dataSize}' bytes exceeds the configured limit`
|
|
83
|
+
})
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Normalize global TTL
|
|
89
|
+
* @type {[number]}
|
|
90
|
+
*/
|
|
91
|
+
const globalTTL = Array.isArray(this.opts.ttl)
|
|
92
|
+
? [this.opts.ttl[0], this.opts.ttl[1] || 0]
|
|
93
|
+
: [this.opts.ttl || 0, 0]
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Normalize incoming TTL
|
|
97
|
+
*/
|
|
98
|
+
let L1TTL = 0
|
|
99
|
+
let L2TTL = 0
|
|
100
|
+
|
|
101
|
+
if (Array.isArray(ttl)) {
|
|
102
|
+
L2TTL = ttl[0]
|
|
103
|
+
L1TTL = ttl[1] || 0
|
|
104
|
+
} else if (typeof ttl === "number") {
|
|
105
|
+
L2TTL = ttl
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const safeL2TTL = Math.min(L2TTL, globalTTL[0], MAX_L2_TTL)
|
|
109
|
+
const safeL1TTL = Math.min(L1TTL, globalTTL[1], MAX_L1_TTL)
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Store in Redis (L2) when safeL2TTL > 0 and safeL2TTL > safeL1TTL
|
|
113
|
+
*/
|
|
114
|
+
if( safeL2TTL > 0 && safeL2TTL > safeL1TTL ){
|
|
115
|
+
await this.redis.set(key, {result:data, ttl:[safeL2TTL, safeL1TTL], txnTs: Date.now()}, safeL2TTL)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Only store in Memory if l1TTL > 0
|
|
120
|
+
*/
|
|
121
|
+
if (safeL1TTL > 0) {
|
|
122
|
+
await this.memory.set(prefixedKey, data, safeL1TTL)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async del(keys) {
|
|
127
|
+
await this.redis.del(keys)
|
|
128
|
+
await this.memory.del(keys)
|
|
129
|
+
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async clean(match) {
|
|
133
|
+
await this.redis.clean(match)
|
|
134
|
+
|
|
135
|
+
let matchs = Array.isArray(match) ? match : [match]
|
|
136
|
+
matchs = matchs.map(el => el.replace("*", ""))
|
|
137
|
+
|
|
138
|
+
for (const patt of matchs){
|
|
139
|
+
const mKeys = this.memory.cache.keys()
|
|
140
|
+
let key = mKeys.next()
|
|
141
|
+
while (!key.done) {
|
|
142
|
+
if ( key.value.includes(patt) ) {
|
|
143
|
+
this.memory.cache.delete(key.value)
|
|
144
|
+
this.logger.debug({
|
|
145
|
+
tag: "SBK",
|
|
146
|
+
message: "REMOVE MEMORY LRU KEY",
|
|
147
|
+
key
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
key = mKeys.next()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { Errors } from "moleculer"
|
|
2
|
+
import ApiGateway from "moleculer-web"
|
|
3
|
+
import jwt from "jsonwebtoken"
|
|
4
|
+
import jwksClient from "jwks-rsa"
|
|
5
|
+
|
|
6
|
+
import * as configHdl from '#hUti/config-handler.mjs'
|
|
7
|
+
|
|
8
|
+
const ACCESS_ROLES = await configHdl.getConfigs('/config/sbk/edge/yaml/roles')
|
|
9
|
+
const OPENID_CONFIG = await configHdl.getConfigs('/config/sbk/edge/yaml/auth')
|
|
10
|
+
export const {clientId} = OPENID_CONFIG
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initializes OpenID Connect configuration and JWKS client.
|
|
14
|
+
*
|
|
15
|
+
* Fetches the OpenID configuration document from the configured
|
|
16
|
+
* issuer endpoint and creates a cached JWKS client for JWT
|
|
17
|
+
* signature verification.
|
|
18
|
+
*
|
|
19
|
+
* The JWKS client supports:
|
|
20
|
+
* - Automatic key retrieval
|
|
21
|
+
* - In-memory caching
|
|
22
|
+
* - Request rate limiting
|
|
23
|
+
*
|
|
24
|
+
* @async
|
|
25
|
+
* @function getOpenidConfigs
|
|
26
|
+
*
|
|
27
|
+
* @throws {Error} Throws if the OpenID configuration request fails
|
|
28
|
+
* or the response cannot be parsed.
|
|
29
|
+
*
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
const getOpenidConfigs = async () => {
|
|
33
|
+
|
|
34
|
+
const response = await fetch(OPENID_CONFIG.url)
|
|
35
|
+
|
|
36
|
+
if ( !response.ok ) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`OpenID configuration endpoint failed with status "${response.status}"`
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return await response.json()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const {
|
|
46
|
+
issuer,
|
|
47
|
+
jwks_uri: jwksUri,
|
|
48
|
+
authorization_endpoint: authorizationEndpoint,
|
|
49
|
+
token_endpoint: tokenEndpoint,
|
|
50
|
+
end_session_endpoint: endSessionEndpoint,
|
|
51
|
+
} = await getOpenidConfigs()
|
|
52
|
+
|
|
53
|
+
const client = jwksClient({
|
|
54
|
+
...OPENID_CONFIG.jwksClient,
|
|
55
|
+
jwksUri: jwksUri,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const issuers = ((iss) => {
|
|
59
|
+
const httpUrl = new URL(iss)
|
|
60
|
+
httpUrl.protocol = "http:"
|
|
61
|
+
httpUrl.port = String(OPENID_CONFIG.kcPort)
|
|
62
|
+
return [
|
|
63
|
+
iss,
|
|
64
|
+
httpUrl.toString(),
|
|
65
|
+
]
|
|
66
|
+
})(issuer)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Retrieves the public signing key corresponding to a JWT `kid`.
|
|
71
|
+
*
|
|
72
|
+
* Used by JWT verification libraries (such as `jsonwebtoken`)
|
|
73
|
+
* to dynamically resolve signing keys from a JWKS endpoint.
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} header - Decoded JWT header.
|
|
76
|
+
* @param {string} header.kid - Key ID used to identify the signing key.
|
|
77
|
+
* @param {Function} callback - Callback function invoked after key retrieval.
|
|
78
|
+
* @param {Error|null} callback.err - Error object if key retrieval fails.
|
|
79
|
+
* @param {string|null} callback.key - PEM formatted public key.
|
|
80
|
+
*
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
const getKey = (header, callback) => {
|
|
84
|
+
|
|
85
|
+
client.getSigningKey(
|
|
86
|
+
header.kid,
|
|
87
|
+
(err, key) => {
|
|
88
|
+
|
|
89
|
+
if (err) {
|
|
90
|
+
callback(err)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
callback(
|
|
95
|
+
null,
|
|
96
|
+
key.publicKey || key.rsaPublicKey
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Authenticates the requester by verifying the access token.
|
|
107
|
+
* The token can be provided either in the Bearer Authorization header
|
|
108
|
+
* or in cookies.
|
|
109
|
+
*
|
|
110
|
+
* @param {Context} ctx - Moleculer service context
|
|
111
|
+
* @param {Object} route - REST route configuration object
|
|
112
|
+
* @param {HTTP request<Object>} req - HTTP request object
|
|
113
|
+
* @returns {Promise<Object>} Authenticated user / authorization result
|
|
114
|
+
*/
|
|
115
|
+
export const authenticate = async (ctx, route, req, res) => {
|
|
116
|
+
|
|
117
|
+
let token = null
|
|
118
|
+
|
|
119
|
+
const auth =
|
|
120
|
+
req.headers.authorization
|
|
121
|
+
|
|
122
|
+
if (auth?.startsWith("Bearer ")) {
|
|
123
|
+
token = auth.replace(/^Bearer\s+/i, "")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
!token &&
|
|
129
|
+
req.cookies?.WbsedclAccessToken
|
|
130
|
+
) {
|
|
131
|
+
token = req.cookies?.WbsedclAccessToken
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if (!token) {
|
|
136
|
+
throw new ApiGateway.Errors.UnAuthorizedError(
|
|
137
|
+
ApiGateway.Errors.ERR_NO_TOKEN
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const decoded =
|
|
143
|
+
await new Promise((resolve, reject) => {
|
|
144
|
+
|
|
145
|
+
jwt.verify(
|
|
146
|
+
token,
|
|
147
|
+
getKey,
|
|
148
|
+
{
|
|
149
|
+
algorithms: OPENID_CONFIG.jwtVerifyAlgo,
|
|
150
|
+
issuers,
|
|
151
|
+
// audience: OPENID_CONFIG.audience,
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
(err, dec) => {
|
|
155
|
+
|
|
156
|
+
if (err) {
|
|
157
|
+
reject(err)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
resolve(dec)
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
ctx.broker.logger.debug({tag: "SBK", message: "Decoded Token Object", token: decoded, OPENID_CONFIG, issuers})
|
|
167
|
+
// ctx.meta.user = {
|
|
168
|
+
// sessionId: decoded?.sid,
|
|
169
|
+
// id: decoded?.sub,
|
|
170
|
+
// userId: (decoded?.sub).split(":").at(-1),
|
|
171
|
+
// username: decoded?.preferred_username,
|
|
172
|
+
// firstName: decoded?.given_name,
|
|
173
|
+
// lastName: decoded?.family_name,
|
|
174
|
+
// fullName: [...new Set(decoded?.name.split(/\s+/))].join(" "),
|
|
175
|
+
// emailIds: decoded?.email,
|
|
176
|
+
// mobileNos: decoded?.mobile_no,
|
|
177
|
+
// userRoles: decoded?.user_role,
|
|
178
|
+
// }
|
|
179
|
+
return decoded
|
|
180
|
+
|
|
181
|
+
} catch (err) {
|
|
182
|
+
ctx.broker.logger.debug({tag: "SBK", OPENID_CONFIG})
|
|
183
|
+
throw new ApiGateway.Errors.UnAuthorizedError(
|
|
184
|
+
ApiGateway.Errors.ERR_INVALID_TOKEN
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Verifies the user's roles and access grants before authorizing
|
|
191
|
+
* access to the requested resource.
|
|
192
|
+
* @param {object} ctx [description]
|
|
193
|
+
* @param {object} route [description]
|
|
194
|
+
* @param {object} req [description]
|
|
195
|
+
* @param {object} res [description]
|
|
196
|
+
* @return {object} [description]
|
|
197
|
+
*/
|
|
198
|
+
export const authorize = async(ctx, route, req, res) => {
|
|
199
|
+
|
|
200
|
+
ctx.broker.logger.debug({tag: "SBK", message: "User Profile"}, ctx.meta.user, route?.path)
|
|
201
|
+
|
|
202
|
+
if( ctx.meta?.user?.preferred_username === undefined ){
|
|
203
|
+
throw new ApiGateway.Errors.UnAuthorizedError(
|
|
204
|
+
ApiGateway.Errors.ERR_INVALID_TOKEN
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
ctx.broker.logger.debug({tag: "SBK", message: "ACCESS_ROLES", ACCESS_ROLES, resource_access: ctx.meta.user.resource_access})
|
|
209
|
+
|
|
210
|
+
// resource_access
|
|
211
|
+
|
|
212
|
+
// const aa = new RegExp("^/system/oidc(?:/.*)?$", "i")
|
|
213
|
+
// if()
|
|
214
|
+
|
|
215
|
+
// if( req.url.startsWith("/system" ){
|
|
216
|
+
|
|
217
|
+
// }
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
// /**
|
|
221
|
+
// * Resticed accessor can't access all resources
|
|
222
|
+
// */
|
|
223
|
+
// if( ctx.meta?.user?.preferred_username == OPENID_CONFIG.rAccessor ){
|
|
224
|
+
// throw new Errors.MoleculerError(
|
|
225
|
+
// "You are not authorized to access this resource",
|
|
226
|
+
// 403,
|
|
227
|
+
// "FORBIDDEN"
|
|
228
|
+
// )
|
|
229
|
+
// }
|
|
230
|
+
|
|
231
|
+
// if( ctx.meta.user?.azp == clientId && req.url.startsWith(OPENID_CONFIG.rRoute)) {
|
|
232
|
+
// throw new Errors.MoleculerError(
|
|
233
|
+
// "You are not authorized to access this resource",
|
|
234
|
+
// 403,
|
|
235
|
+
// "FORBIDDEN"
|
|
236
|
+
// )
|
|
237
|
+
// }
|
|
238
|
+
|
|
239
|
+
// if( ctx.meta.user?.azp == OPENID_CONFIG.rAccessor && !req.url.startsWith(OPENID_CONFIG.rRoute)) {
|
|
240
|
+
// throw new Errors.MoleculerError(
|
|
241
|
+
// "You are not authorized to access this resource",
|
|
242
|
+
// 403,
|
|
243
|
+
// "FORBIDDEN"
|
|
244
|
+
// )
|
|
245
|
+
// }
|
|
246
|
+
|
|
247
|
+
return Promise.resolve(ctx)
|
|
248
|
+
}
|
|
249
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export const auxBrokerParamsValidator = (paramObj) => {
|
|
2
|
+
|
|
3
|
+
let serviceNamePrefix = "producer"
|
|
4
|
+
const optsKeys = Object.keys(paramObj.opts || {})
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Callback function.
|
|
8
|
+
*
|
|
9
|
+
* @callback Callback
|
|
10
|
+
* @param {...*} args - Arguments passed to the callback.
|
|
11
|
+
* @returns {boolean} Returns `true` by default when the original callback returns `null` or `undefined`.
|
|
12
|
+
*/
|
|
13
|
+
if (typeof paramObj.callback !== 'function') {
|
|
14
|
+
paramObj.callback = () => true
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const originalCallback = paramObj.callback
|
|
18
|
+
|
|
19
|
+
paramObj.callback = (...args) => {
|
|
20
|
+
return originalCallback(...args) ?? true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Throws an error when type is undefined
|
|
26
|
+
*/
|
|
27
|
+
if ( !paramObj.type ) {
|
|
28
|
+
throw new Error("The \"type\" argument is missing.")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Determines and assigns the service name based on the provided options.
|
|
34
|
+
*
|
|
35
|
+
* Behavior:
|
|
36
|
+
* - If no option keys are provided:
|
|
37
|
+
* - Sets the service type prefix to `"producer"`.
|
|
38
|
+
* - Automatically generates the service name using the format:
|
|
39
|
+
* `"producer-<type>"`.
|
|
40
|
+
*
|
|
41
|
+
* - If option keys are present:
|
|
42
|
+
* - Ensures that `opts.name` is explicitly provided.
|
|
43
|
+
* - Throws an error if the `name` key is missing.
|
|
44
|
+
* - Sets the service type prefix to `"consumer"` when the
|
|
45
|
+
* `"consumer"` option is detected.
|
|
46
|
+
*
|
|
47
|
+
* @throws {Error}
|
|
48
|
+
* Thrown when option keys are provided but the `name` key
|
|
49
|
+
* is missing in `paramObj.opts`.
|
|
50
|
+
*/
|
|
51
|
+
if ( optsKeys.length === 0 ) {
|
|
52
|
+
serviceNamePrefix = "producer"
|
|
53
|
+
paramObj.opts.name = `${serviceNamePrefix}-${paramObj.type}`
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
if( !("name" in paramObj.opts) ){
|
|
57
|
+
throw new Error("Missing required key \"name\" in optins.")
|
|
58
|
+
}
|
|
59
|
+
if ( optsKeys.includes("consumer") ) {
|
|
60
|
+
serviceNamePrefix = "consumer"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Builds and normalizes the service name and topic pattern
|
|
67
|
+
* from the provided service configuration.
|
|
68
|
+
*
|
|
69
|
+
* Processing steps:
|
|
70
|
+
* - Creates a unique name segment list using:
|
|
71
|
+
* - `serviceNamePrefix`
|
|
72
|
+
* - `paramObj.type`
|
|
73
|
+
* - Parts extracted from `paramObj.opts.name`
|
|
74
|
+
* - Removes duplicate segments using `Set`.
|
|
75
|
+
*
|
|
76
|
+
* Topic generation:
|
|
77
|
+
* - If more than two segments exist:
|
|
78
|
+
* - Uses segments after the first two as topic names.
|
|
79
|
+
* - Converts topic segments to uppercase.
|
|
80
|
+
* - Converts a trailing `*` wildcard into Kafka-style `.*`.
|
|
81
|
+
* - Joins topic parts using `"."`.
|
|
82
|
+
* - Otherwise:
|
|
83
|
+
* - Uses the default topic pattern `".*"`.
|
|
84
|
+
*
|
|
85
|
+
* Consumer-specific behavior:
|
|
86
|
+
* - When the service type is `"consumer"`:
|
|
87
|
+
* - Appends `consumer.groupId` to the final service name.
|
|
88
|
+
* - Throws an error if `groupId` is missing.
|
|
89
|
+
*
|
|
90
|
+
* Final name normalization:
|
|
91
|
+
* - Joins all name segments using `"-"`.
|
|
92
|
+
* - Removes trailing `*` and trailing `-` characters.
|
|
93
|
+
*
|
|
94
|
+
* @throws {Error}
|
|
95
|
+
* Thrown when the service is a consumer and
|
|
96
|
+
* `consumer.groupId` is not defined.
|
|
97
|
+
*/
|
|
98
|
+
const splitName = [ ...new Set(
|
|
99
|
+
[ ...[serviceNamePrefix, paramObj.type],
|
|
100
|
+
...paramObj.opts.name.split("-"),
|
|
101
|
+
])
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
if( splitName.length > 2 ) {
|
|
105
|
+
const topics = splitName.slice(2).map(e => e.toUpperCase())
|
|
106
|
+
topics[topics.length - 1] = topics.at(-1)?.replace(/\*$/, ".*")
|
|
107
|
+
paramObj.opts.topicRegx = topics.join(".")
|
|
108
|
+
}
|
|
109
|
+
else{
|
|
110
|
+
paramObj.opts.topicRegx = '.*'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
paramObj.opts.name = splitName.join("-")
|
|
114
|
+
.replace(/\*$/, "")
|
|
115
|
+
.replace(/\-$/, "")
|
|
116
|
+
|
|
117
|
+
if( splitName[0] == "consumer" ) {
|
|
118
|
+
if( paramObj.opts?.consumer?.groupId !== undefined ) {
|
|
119
|
+
paramObj.opts.name = `${paramObj.opts.name}-${paramObj.opts?.consumer?.groupId}`
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
throw new Error("The \"groupId\" key is missing in the consumer options.")
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
}
|