samanbayaka 0.0.15 → 0.0.17

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 CHANGED
@@ -35,7 +35,7 @@ Once the package is installed, you can import the library using import or requir
35
35
  import sbk from "samanbayaka"
36
36
  ```
37
37
  # Prerequisites
38
- It is assumed that NATS, Redpanda, and Redis are installed and properly configured. All services should be discoverable through the /etc/hosts file.
38
+ It is assumed that NATS, Redpanda, Redis, and Etcd are installed and properly configured. All services should be discoverable through the /etc/hosts file.
39
39
  #### Minimum required Node.js version: >= 22.x.x
40
40
  ```bash
41
41
  node -v
@@ -256,7 +256,7 @@ pnpm install samanbayaka
256
256
  touch index.mjs
257
257
  ```
258
258
 
259
- Open index.mjs and paste the following:
259
+ Open `index.mjs` and paste the following:
260
260
  ```bash
261
261
  import sbk from "samanbayaka"
262
262
  await sbk.loadGatewayService()
@@ -280,7 +280,7 @@ pnpm install samanbayaka
280
280
  touch index.mjs
281
281
  ```
282
282
 
283
- Open index.mjs and paste the following:
283
+ Open `index.mjs` and paste the following:
284
284
  ```bash
285
285
  import sbk from "samanbayaka"
286
286
  await sbk.loadFeatureService({
@@ -339,7 +339,7 @@ pnpm install samanbayaka
339
339
  touch index.mjs
340
340
  ```
341
341
 
342
- Open index.mjs and paste the following:
342
+ Open `index.mjs` and paste the following:
343
343
  ```bash
344
344
  import sbk from "samanbayaka"
345
345
  await sbk.auxBrokerService(
@@ -372,13 +372,13 @@ touch index.mjs
372
372
  ```
373
373
  An auxiliary service, such as a `consumer` that subscribe messages from the `STUDENT` topic having group name `junior`, can be created as follows:
374
374
  ```js
375
- mkdir producer-kafka-student-junior
376
- cd producer-kafka-student-junior
375
+ mkdir consumer-kafka-student-junior
376
+ cd consumer-kafka-student-junior
377
377
  pnpm init
378
378
  pnpm install samanbayaka
379
379
  touch index.mjs
380
380
  ```
381
- Open index.mjs and paste the following:
381
+ Open `index.mjs` and paste the following:
382
382
  ```bash
383
383
  import sbk from "samanbayaka"
384
384
  await sbk.auxBrokerService(
@@ -426,7 +426,7 @@ pnpm init
426
426
  pnpm install samanbayaka
427
427
  touch demo.mjs
428
428
  ```
429
- Open and edit the demo.mjs file as follows
429
+ Open and edit the `demo.mjs` file as follows
430
430
  ```bash
431
431
  import sbk from "samanbayaka"
432
432
  await sbk.loadDemo()
@@ -436,6 +436,11 @@ Run the service, demo
436
436
  node demo.mjs
437
437
  ```
438
438
 
439
+ ## Documentation
440
+
441
+ * [Dockers]
442
+ * [etcd](docs/dockers/etcd.md)
443
+
439
444
  <p align="center" style="margin-top: 100px;">
440
445
  <img src="https://moleculer.services/images/banner.png" alt="Moleculer Logo" width="600">
441
446
  </p>
package/commit-hash.mjs CHANGED
@@ -1 +1 @@
1
- export const COMMIT_HASH = '2205c3b';
1
+ export const COMMIT_HASH = '7052c65';
@@ -0,0 +1,126 @@
1
+ # Dockers
2
+ ## Etcd
3
+ To manage configurations centrally, use the docker-compose.yml file to run Etcd.
4
+ ```bash
5
+ mkdir -p etcd
6
+ cd etcd
7
+ touch docker-compose.yml
8
+ export ETCD_ROOT_PW=<your_root_pass>
9
+ export ETCD_CONFIG_ADMIN_PW=<your_config_admin_pass>
10
+ ```
11
+
12
+ Open `docker-compose.yml` and paste the following:
13
+ ```yml
14
+ services:
15
+ etcd:
16
+ image: quay.io/coreos/etcd:v3.5.5
17
+ container_name: sbk-etcd
18
+
19
+ ports:
20
+ - "<your_api_port>:2379"
21
+ - "<your_cluster_port>:2380"
22
+
23
+ volumes:
24
+ - /usr/local/etc/samanbayaka:/etcd-data
25
+
26
+ command:
27
+ - /usr/local/bin/etcd
28
+ - --name=etcd
29
+ - --data-dir=/etcd-data
30
+ - --listen-client-urls=http://0.0.0.0:2379
31
+ - --advertise-client-urls=http://etcd:2379
32
+ - --listen-peer-urls=http://0.0.0.0:2380
33
+ - --initial-advertise-peer-urls=http://etcd:2380
34
+ - --initial-cluster=etcd=http://etcd:2380
35
+ - --initial-cluster-state=new
36
+
37
+ restart: unless-stopped
38
+
39
+ etcd-init:
40
+ image: quay.io/coreos/etcd:v3.5.5
41
+ depends_on:
42
+ - etcd
43
+
44
+ volumes:
45
+ - /usr/local/etc/samanbayaka:/etcd-data
46
+
47
+ environment:
48
+ - ETCDCTL_API=3
49
+
50
+ entrypoint:
51
+ - /bin/sh
52
+ - -c
53
+
54
+ command:
55
+ - |
56
+ set -e
57
+
58
+ echo "waiting for etcd..."
59
+
60
+ for i in $(seq 1 30); do
61
+ etcdctl --endpoints=http://etcd:2379 endpoint health && break
62
+ echo "retry $$i..."
63
+ sleep 2
64
+ done
65
+
66
+ echo "etcd initialized"
67
+
68
+ echo "Creating the root role"
69
+ etcdctl --endpoints=http://etcd:2379 role add root || true
70
+
71
+ echo "Creating the root user"
72
+ etcdctl --endpoints=http://etcd:2379 user add root:${ETCD_ROOT_PW} || true
73
+
74
+ echo "Granting the root role to the root user"
75
+ etcdctl --endpoints=http://etcd:2379 user grant-role root root
76
+
77
+ echo "Enable authentication"
78
+ etcdctl --endpoints=http://etcd:2379 auth enable || true
79
+
80
+ # -------------------------------------------------------
81
+
82
+ echo "Creating admin role"
83
+ etcdctl --endpoints=http://etcd:2379 --user=root:${ETCD_ROOT_PW} role add admin
84
+
85
+ echo "Granting broad permissions"
86
+ etcdctl --endpoints=http://etcd:2379 --user=root:${ETCD_ROOT_PW} role grant-permission --prefix=true admin readwrite /
87
+
88
+ echo "Creating configadmin user"
89
+ etcdctl --endpoints=http://etcd:2379 --user=root:${ETCD_ROOT_PW} user add configadmin:${ETCD_CONFIG_ADMIN_PW}
90
+
91
+ echo "Assign role"
92
+ etcdctl --endpoints=http://etcd:2379 --user=root:${ETCD_ROOT_PW} user grant-role configadmin admin
93
+
94
+ # ---------------------------------------------------------
95
+
96
+ echo "Creating Roles and Granting"
97
+ etcdctl --endpoints=http://etcd:2379 --user=root:${ETCD_ROOT_PW} role add gateway
98
+ etcdctl --endpoints=http://etcd:2379 --user=root:${ETCD_ROOT_PW} role grant-permission --prefix=true gateway read /config/yaml/nats
99
+ etcdctl --endpoints=http://etcd:2379 --user=root:${ETCD_ROOT_PW} role grant-permission --prefix=true gateway read /config/yaml/auth
100
+ etcdctl --endpoints=http://etcd:2379 --user=root:${ETCD_ROOT_PW} role grant-permission --prefix=true gateway read /config/yaml/catch
101
+ etcdctl --endpoints=http://etcd:2379 --user=root:${ETCD_ROOT_PW} role grant-permission --prefix=true gateway read /config/yaml/telemetry
102
+
103
+ # ---------------------------------------------------------
104
+
105
+ echo "Creating User and Assign Role"
106
+ etcdctl --endpoints=http://etcd:2379 --user=root:${ETCD_ROOT_PW} user add <your_user>:<your_pass>
107
+ etcdctl --endpoints=http://etcd:2379 --user=root:${ETCD_ROOT_PW} user grant-role apigtwy gateway
108
+
109
+ # ---------------------------------------------------------
110
+
111
+ etcdctl --endpoints=http://etcd:2379 --user=configadmin:${ETCD_CONFIG_ADMIN_PW} put /config/yaml/nats "$(cat /etcd-data/nats.yml)" || true
112
+
113
+ #etcdctl --endpoints=http://etcd:2379 --user=<your_user>:<your_pass> get /config/yaml/nats
114
+
115
+
116
+ echo "etcd initialization completed"
117
+ ```
118
+ Optionally verify:
119
+ ```bash
120
+ docker ps | grep 'sbk-etcd'
121
+ ```
122
+ Optionally check health:
123
+ ```bash
124
+ docker exec sbk-etcd etcdctl --endpoints=http://etcd:2379 endpoint health
125
+ ```
126
+ ---
@@ -0,0 +1,171 @@
1
+ import ApiGateway from "moleculer-web"
2
+ import jwt from "jsonwebtoken"
3
+ import jwksClient from "jwks-rsa"
4
+
5
+ const OPENID_CONFIG = {
6
+ url: "https://accounts.google.com/.well-known/openid-configuration",
7
+ clientId: "184045176764-ugg28aegdro383pintufun14uubtt374.apps.googleusercontent.com",
8
+ jwksClient: {
9
+ cache: true,
10
+ cacheMaxEntries: 5,
11
+ cacheMaxAge: 10 * 60 * 1000,
12
+ rateLimit: true,
13
+ jwksRequestsPerMinute: 10,
14
+ },
15
+ jwtVerifyAlgo: ["RS256"],
16
+ client: {},
17
+ issuer: "",
18
+ }
19
+
20
+ // let client
21
+ // let issuer
22
+
23
+ /**
24
+ * Retrieves the public signing key corresponding to a JWT `kid`.
25
+ *
26
+ * Used by JWT verification libraries (such as `jsonwebtoken`)
27
+ * to dynamically resolve signing keys from a JWKS endpoint.
28
+ *
29
+ * @param {Object} header - Decoded JWT header.
30
+ * @param {string} header.kid - Key ID used to identify the signing key.
31
+ * @param {Function} callback - Callback function invoked after key retrieval.
32
+ * @param {Error|null} callback.err - Error object if key retrieval fails.
33
+ * @param {string|null} callback.key - PEM formatted public key.
34
+ *
35
+ * @returns {void}
36
+ */
37
+ const getKey = (header, callback) => {
38
+
39
+ OPENID_CONFIG.client.getSigningKey(
40
+ header.kid,
41
+ (err, key) => {
42
+
43
+ if (err) {
44
+ callback(err)
45
+ return
46
+ }
47
+
48
+ callback(
49
+ null,
50
+ key.publicKey || key.rsaPublicKey
51
+ )
52
+ }
53
+ )
54
+
55
+ }
56
+
57
+
58
+ /**
59
+ * Initializes OpenID Connect configuration and JWKS client.
60
+ *
61
+ * Fetches the OpenID configuration document from the configured
62
+ * issuer endpoint and creates a cached JWKS client for JWT
63
+ * signature verification.
64
+ *
65
+ * The JWKS client supports:
66
+ * - Automatic key retrieval
67
+ * - In-memory caching
68
+ * - Request rate limiting
69
+ *
70
+ * @async
71
+ * @function initOpenId
72
+ *
73
+ * @throws {Error} Throws if the OpenID configuration request fails
74
+ * or the response cannot be parsed.
75
+ *
76
+ * @returns {Promise<void>}
77
+ */
78
+ export const initOpenId = async () => {
79
+
80
+ const response = await fetch(OPENID_CONFIG.url)
81
+
82
+ if ( !response.ok ) {
83
+ throw new Error(
84
+ `OpenID configuration endpoint failed with status "${response.status}"`
85
+ )
86
+ }
87
+
88
+ const config = await response.json()
89
+
90
+ OPENID_CONFIG.issuer = config.issuer
91
+
92
+ OPENID_CONFIG.client = jwksClient({
93
+ ...OPENID_CONFIG.jwksClient,
94
+ ...{jwksUri: config.jwks_uri},
95
+ })
96
+
97
+ }
98
+
99
+
100
+ /**
101
+ * Authorizes the requester by validating the access token.
102
+ * The token can be provided either in the Bearer Authorization header
103
+ * or in cookies.
104
+ *
105
+ * @param {Context} ctx - Moleculer service context
106
+ * @param {Object} route - REST route configuration object
107
+ * @param {HTTP request<Object>} req - HTTP request object
108
+ * @returns {Promise<Object>} Authenticated user / authorization result
109
+ */
110
+ export const authorize = async (ctx, route, req) => {
111
+
112
+ let token = null
113
+
114
+ const auth =
115
+ req.headers.authorization
116
+
117
+ if (auth?.startsWith("Bearer ")) {
118
+ token = auth.replace(/^Bearer\s+/i, "")
119
+ }
120
+
121
+
122
+ if (
123
+ !token &&
124
+ req.cookies?.id_token
125
+ ) {
126
+ token = req.cookies.id_token
127
+ }
128
+
129
+ if (!token) {
130
+ throw new ApiGateway.Errors.UnAuthorizedError(
131
+ ApiGateway.Errors.ERR_NO_TOKEN
132
+ )
133
+ }
134
+
135
+ try {
136
+
137
+ const decoded =
138
+ await new Promise((resolve, reject) => {
139
+
140
+ jwt.verify(
141
+ token,
142
+ getKey,
143
+ {
144
+ algorithms: OPENID_CONFIG.jwtVerifyAlgo,
145
+ issuer: OPENID_CONFIG.issuer,
146
+ audience: OPENID_CONFIG.clientId,
147
+ },
148
+
149
+ (err, decoded) => {
150
+
151
+ if (err) {
152
+ reject(err)
153
+ return
154
+ }
155
+
156
+ resolve(decoded)
157
+ }
158
+ )
159
+ })
160
+
161
+ ctx.broker.logger.debug({message: "Decoded Token Object", token: decoded})
162
+ ctx.meta.user = { name: decoded?.name, email: decoded?.email }
163
+ return decoded
164
+
165
+ } catch (err) {
166
+ throw new ApiGateway.Errors.UnAuthorizedError(
167
+ ApiGateway.Errors.ERR_INVALID_TOKEN
168
+ )
169
+ }
170
+ }
171
+
package/index.mjs CHANGED
@@ -19,7 +19,6 @@ import HybridCacher from "#hMol/HybridCacher.mjs"
19
19
  import about from '#sAbt/index.mjs'
20
20
  import apiGateway from '#sApi/index.mjs'
21
21
  import { auxBrokerParamsValidator } from '#hUti/aux-broker-params-validator.mjs'
22
-
23
22
  import * as kafka from '#sKfk/index.mjs'
24
23
  import * as demo from '#sDmo/index.mjs'
25
24
 
@@ -27,7 +26,6 @@ import * as demo from '#sDmo/index.mjs'
27
26
  const BROKER_CONFIG = await getConfig('nats')
28
27
  const CATCHER_CONFIG = await getConfig('catcher')
29
28
  const TELEMETRY_CONFIG = await getConfig('telemetry')
30
- const KAFKA_CONFIG = await getConfig('kafkajs')
31
29
 
32
30
  const sbkLoggers = {
33
31
  "CustomPino": customLogger
@@ -155,7 +153,7 @@ export default {
155
153
  auxBroker = kafka
156
154
  }
157
155
  else {
158
- throw new Error("The \"type\" value must be either \"kafka\" or \"mqtt\".")
156
+ throw new Error("The \"type\" value must be either \"kafka\" or \"mqtt\" or \"amqp\"")
159
157
  }
160
158
 
161
159
  opts.logLevel = logLevel
@@ -179,7 +177,6 @@ export default {
179
177
  )
180
178
  }
181
179
 
182
-
183
180
  /**
184
181
  * Start broker
185
182
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "samanbayaka",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Moleculer Gateway service with kafka transporter",
5
5
  "homepage": "https://gitlab.com/dalal.suvendu/samanbayaka#readme",
6
6
  "bugs": {
@@ -43,6 +43,8 @@
43
43
  "cookie-parser": "^1.4.7",
44
44
  "helmet": "^8.1.0",
45
45
  "ioredis": "^5.10.1",
46
+ "jsonwebtoken": "^9.0.3",
47
+ "jwks-rsa": "^4.0.1",
46
48
  "kafkajs": "^2.2.4",
47
49
  "lru-cache": "^11.3.5",
48
50
  "moleculer": "0.15.0",
@@ -0,0 +1,93 @@
1
+ <button onclick="login()">Login with Google</button>
2
+ <button onclick="callAPI()">Call API</button>
3
+
4
+ <script>
5
+ const CLIENT_ID = "184045176764-fmdarpud3fkpojo5p73mloetgrqrg65p.apps.googleusercontent.com";
6
+ const REDIRECT_URI = "http://localhost:3000";
7
+ const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
8
+ const TOKEN_URL = "https://oauth2.googleapis.com/token";
9
+
10
+ // --- PKCE helpers ---
11
+ function generateRandomString(length) {
12
+ const chars =
13
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
14
+ let result = "";
15
+ for (let i = 0; i < length; i++) {
16
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
17
+ }
18
+ return result;
19
+ }
20
+
21
+ async function sha256(verifier) {
22
+ const encoder = new TextEncoder();
23
+ const data = encoder.encode(verifier);
24
+ const hash = await crypto.subtle.digest("SHA-256", data);
25
+ return btoa(String.fromCharCode(...new Uint8Array(hash)))
26
+ .replace(/\+/g, "-")
27
+ .replace(/\//g, "_")
28
+ .replace(/=+$/, "");
29
+ }
30
+
31
+ // --- Step 1: login redirect ---
32
+ async function login() {
33
+ const codeVerifier = generateRandomString(64);
34
+ const codeChallenge = await sha256(codeVerifier);
35
+
36
+ localStorage.setItem("code_verifier", codeVerifier);
37
+
38
+ const url =
39
+ `${AUTH_URL}?response_type=code` +
40
+ `&client_id=${CLIENT_ID}` +
41
+ `&redirect_uri=${REDIRECT_URI}` +
42
+ `&scope=openid%20email%20profile` +
43
+ `&code_challenge=${codeChallenge}` +
44
+ `&code_challenge_method=S256`;
45
+
46
+ window.location = url;
47
+ }
48
+
49
+ // --- Step 2: handle redirect ---
50
+ async function handleRedirect() {
51
+ const params = new URLSearchParams(window.location.search);
52
+ const code = params.get("code");
53
+
54
+ if (!code) return;
55
+
56
+ const codeVerifier = localStorage.getItem("code_verifier");
57
+
58
+ const res = await fetch(TOKEN_URL, {
59
+ method: "POST",
60
+ headers: {
61
+ "Content-Type": "application/x-www-form-urlencoded",
62
+ },
63
+ body: new URLSearchParams({
64
+ client_id: CLIENT_ID,
65
+ grant_type: "authorization_code",
66
+ code,
67
+ code_verifier: codeVerifier,
68
+ redirect_uri: REDIRECT_URI,
69
+ }),
70
+ });
71
+
72
+ const data = await res.json();
73
+
74
+ console.log("Access Token:", data.access_token);
75
+
76
+ localStorage.setItem("access_token", data.access_token);
77
+ }
78
+
79
+ // --- Step 3: call API ---
80
+ async function callAPI() {
81
+ const token = localStorage.getItem("access_token");
82
+
83
+ const res = await fetch("http://localhost:4000/api/resource", {
84
+ headers: {
85
+ Authorization: `Bearer ${token}`,
86
+ },
87
+ });
88
+
89
+ console.log(await res.json());
90
+ }
91
+
92
+ handleRedirect();
93
+ </script>
@@ -3,7 +3,7 @@ import { pkgDtls } from '#hFil/esm-loading.mjs'
3
3
  export default {
4
4
  name: "about",
5
5
  actions: {
6
- me: {
6
+ project: {
7
7
  rest: {
8
8
  method: "GET",
9
9
  path: "/"
@@ -34,5 +34,15 @@ export default {
34
34
  return `${pkgDtls.fullName} ${pkgDtls.version}`
35
35
  },
36
36
  },
37
+ profile: {
38
+ rest: {
39
+ method: "GET",
40
+ path: "/me"
41
+ },
42
+
43
+ handler: async(ctx) => {
44
+ return ctx.meta.user
45
+ }
46
+ }
37
47
  },
38
48
  }
@@ -0,0 +1,37 @@
1
+ import { pkgDtls } from '#hFil/esm-loading.mjs'
2
+
3
+ export default {
4
+ name: "bff",
5
+ actions: {
6
+ signin: {
7
+ rest: {
8
+ method: "GET",
9
+ path: "/signin"
10
+ },
11
+
12
+ handler: async(ctx) => {
13
+ return `Welcome to ${pkgDtls.name} is a microservices-based project built with moleculer and moleculer-web, designed for scalable, high-performance service communication and API management.`
14
+ },
15
+ },
16
+ callback: {
17
+ rest: {
18
+ method: "GET",
19
+ path: "/callback"
20
+ },
21
+
22
+ handler: async(ctx) => {
23
+ return `${pkgDtls.fullName} ${pkgDtls.version}`
24
+ },
25
+ },
26
+ signout: {
27
+ rest: {
28
+ method: "GET",
29
+ path: "/signout"
30
+ },
31
+
32
+ handler: async(ctx) => {
33
+ return ctx.meta.user
34
+ }
35
+ }
36
+ },
37
+ }
@@ -1,4 +1,4 @@
1
- /*index.mjs*/
1
+ // index.mjs
2
2
  import ApiGateway from "moleculer-web"
3
3
  import OpenApi from "moleculer-auto-openapi"
4
4
  import cookieParser from "cookie-parser"
@@ -6,6 +6,7 @@ import helmet from "helmet"
6
6
  import compression from "compression"
7
7
 
8
8
  import {formatHttpErrors} from '#hUti/error-handler.mjs'
9
+ import { initOpenId, authorize } from '#hUti/access-token-validator.mjs'
9
10
 
10
11
  export default {
11
12
  name: "gateway",
@@ -13,6 +14,13 @@ export default {
13
14
  ApiGateway,
14
15
  ],
15
16
 
17
+ async started() {
18
+ /**
19
+ * Initialize OpenID config
20
+ */
21
+ await initOpenId()
22
+ },
23
+
16
24
  settings: {
17
25
  port: process.env.SBK_PORT || 8765,
18
26
 
@@ -42,14 +50,33 @@ export default {
42
50
 
43
51
 
44
52
  routes: [
53
+ // Public routes
54
+ {
55
+ path: "/",
56
+ authorization: false,
57
+
58
+ whitelist: [
59
+ "bff.*",
60
+ ],
61
+
62
+ autoAliases: true,
63
+ mappingPolicy: "restrict",
64
+
65
+ bodyParsers: {
66
+ json: true,
67
+ urlencoded: { extended: true }
68
+ }
69
+ },
70
+
45
71
  /**
46
- * API routes
72
+ * Protected API routes
47
73
  */
48
74
  {
49
75
  path: "/api",
76
+ authorization: true,
50
77
  whitelist: [
51
78
  /**
52
- * Access any actions except 'system' service
79
+ * Access any actions except 'gateway' and 'system' service
53
80
  */
54
81
  /^(?!(gateway|system)\.)\w+(?:\.\w+)*$/
55
82
  ],
@@ -124,6 +151,7 @@ export default {
124
151
  // this.logger.error(" Request error!", err.name, ":", err.message, "\n", err.stack, "\nData:", err.data);
125
152
  // }
126
153
  this.sendError(req, res, err)
127
- }
154
+ },
155
+ authorize,
128
156
  },
129
157
  }
@@ -196,7 +196,7 @@ export const consumer = (opts, callback) => {
196
196
 
197
197
  if(interMessageDelayMs > 0 ){
198
198
  // control message reading per second
199
- await delay(1000)
199
+ await delay(interMessageDelayMs)
200
200
  }
201
201
  }
202
202