badmfck-api-server 4.1.29 → 4.1.31

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.
@@ -146,14 +146,18 @@ class APIService extends BaseService_1.BaseService {
146
146
  this.options.endpoints.push(new Documentation_1.Documentation(this.options));
147
147
  this.options.endpoints.push(new ExternalServiceEndpoint_1.ExternalServiceEndpoint());
148
148
  this.options.endpoints.push(new MicroserviceHostController_1.MicroserviceHostController());
149
- new DocumentService_1.DocumentGenerator(this.options.endpoints);
150
- this.monitor = new MonitorService_1.MonitorService();
151
- this.monitor.init();
152
- if (this.options.deployer && this.options.deployer.users.length > 0) {
153
- this.options.endpoints.push(new Deployer_1.Deployer());
149
+ if (this.options.deployer && this.options.deployer.projects_path && this.options.deployer.setup_path) {
154
150
  this.deployer = new DeployerService_1.DeployerService(this.options.deployer);
155
151
  this.deployer.init();
152
+ this.options.endpoints = [
153
+ new Deployer_1.Deployer(),
154
+ new Documentation_1.Documentation(this.options),
155
+ new Monitor_1.Monitor(this.options),
156
+ ];
156
157
  }
158
+ this.monitor = new MonitorService_1.MonitorService();
159
+ this.monitor.init();
160
+ new DocumentService_1.DocumentGenerator(this.options.endpoints);
157
161
  this.options.endpoints.push(new Liveness_1.Liveness(this.started), new Readiness_1.Readiness(this.started));
158
162
  exports.REQ_HTTP_REQUESTS_COUNT.listener = async () => this.requestsCount;
159
163
  }
@@ -48,7 +48,12 @@ declare const _TConfig: {
48
48
  readonly $__on_switch_optional: true;
49
49
  };
50
50
  readonly $__bluegreen_optional: true;
51
- readonly nginx: {};
51
+ readonly nginx: {
52
+ readonly config: "";
53
+ readonly $__config_optional: true;
54
+ readonly symlink: "";
55
+ readonly $__symlink_optional: true;
56
+ };
52
57
  readonly $__nginx_optional: true;
53
58
  };
54
59
  export type IConfig = ValidationModel<typeof _TConfig>;
@@ -63,7 +68,10 @@ export declare const REQ_DEPLOYMENT_CONFIG: Req<void, Map<string, {
63
68
  blue_config: {};
64
69
  green_config: {};
65
70
  } | undefined;
66
- nginx?: {} | undefined;
71
+ nginx?: {
72
+ config?: string | undefined;
73
+ symlink?: string | undefined;
74
+ } | undefined;
67
75
  name: string;
68
76
  destination: string;
69
77
  description: string;
@@ -89,7 +97,10 @@ export declare const REQ_DEPLOYMENT_CONFIG_ADD: Req<{
89
97
  blue_config: {};
90
98
  green_config: {};
91
99
  } | undefined;
92
- nginx?: {} | undefined;
100
+ nginx?: {
101
+ config?: string | undefined;
102
+ symlink?: string | undefined;
103
+ } | undefined;
93
104
  name: string;
94
105
  destination: string;
95
106
  description: string;
@@ -114,7 +125,10 @@ export declare const REQ_DEPLOYMENT_CONFIG_ADD: Req<{
114
125
  blue_config: {};
115
126
  green_config: {};
116
127
  } | undefined;
117
- nginx?: {} | undefined;
128
+ nginx?: {
129
+ config?: string | undefined;
130
+ symlink?: string | undefined;
131
+ } | undefined;
118
132
  name: string;
119
133
  destination: string;
120
134
  description: string;
@@ -79,7 +79,12 @@ const _TConfig = {
79
79
  $__on_switch_optional: true,
80
80
  },
81
81
  $__bluegreen_optional: true,
82
- nginx: {},
82
+ nginx: {
83
+ config: "",
84
+ $__config_optional: true,
85
+ symlink: "",
86
+ $__symlink_optional: true,
87
+ },
83
88
  $__nginx_optional: true,
84
89
  };
85
90
  exports.REQ_DEPLOYMENT_CONFIG = new badmfck_signal_1.Req();
@@ -1,6 +1,7 @@
1
1
  import { BaseEndpoint } from "../BaseEndpoint";
2
2
  import { HTTPRequestVO, TransferPacketVO } from "../structures/Interfaces";
3
3
  export declare class Deployer extends BaseEndpoint {
4
+ ignoreInDocumentation: boolean;
4
5
  constructor();
5
6
  file(req: HTTPRequestVO): Promise<TransferPacketVO<any>>;
6
7
  switchSlot(req: HTTPRequestVO): Promise<TransferPacketVO<any>>;
@@ -1,9 +1,14 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.Deployer = void 0;
4
7
  const BaseEndpoint_1 = require("../BaseEndpoint");
8
+ const DefaultErrors_1 = __importDefault(require("../structures/DefaultErrors"));
5
9
  const DeployerService_1 = require("./DeployerService");
6
10
  class Deployer extends BaseEndpoint_1.BaseEndpoint {
11
+ ignoreInDocumentation = true;
7
12
  constructor() {
8
13
  super("pckg");
9
14
  this.registerEndpoints([
@@ -41,7 +46,7 @@ class Deployer extends BaseEndpoint_1.BaseEndpoint {
41
46
  }
42
47
  async file(req) {
43
48
  if (!req.files)
44
- return { error: { code: 10, message: "No files were uploaded.", httpStatus: 400 } };
49
+ throw { ...DefaultErrors_1.default.FILE_NOT_EXISTS, details: "No files were uploaded" };
45
50
  const result = await DeployerService_1.REQ_DEPLOYMENT_PROCEED.request({ ...req.data, files: req.files, auth: req.headers.authorization });
46
51
  return { data: result };
47
52
  }
@@ -1,19 +1,27 @@
1
1
  import { Req } from "badmfck-signal";
2
2
  import { BaseService } from "../BaseService";
3
- import { IDeployerUser } from "../APIService";
4
3
  import { IConfig, IProjectState } from "./ConfigService";
5
4
  import { UploadedFile } from "express-fileupload";
5
+ import { ValidationModel } from "../..";
6
6
  import { IDeploymentNotifier } from "./Notifier";
7
7
  import { IError } from "../structures/Interfaces";
8
+ declare const _TSetup: {
9
+ readonly users: readonly [{
10
+ readonly login: "";
11
+ readonly password: "";
12
+ }];
13
+ readonly watchdog: true;
14
+ readonly $__watchdog_optional: true;
15
+ readonly notifier: {
16
+ readonly URL: "";
17
+ readonly KEY: "";
18
+ };
19
+ readonly $__notifier_optional: true;
20
+ };
21
+ export type TDeployerSetup = ValidationModel<typeof _TSetup>;
8
22
  export interface IDeployerServiceOptions {
9
23
  projects_path: string;
10
- users: IDeployerUser[];
11
- watchdog?: boolean;
12
- notifier?: {
13
- URL?: string;
14
- KEY?: string;
15
- controller?: BaseService;
16
- } | null;
24
+ setup_path: string;
17
25
  }
18
26
  export declare const REQ_DEPLOYMENT_PROCEED: Req<{
19
27
  auth: string;
@@ -35,6 +43,7 @@ export declare const REQ_DEPLOYMENT_STATUS: Req<{
35
43
  }, any>;
36
44
  export declare class DeployerService extends BaseService {
37
45
  options: IDeployerServiceOptions;
46
+ setup: TDeployerSetup | null;
38
47
  notifier: IDeploymentNotifier | null;
39
48
  busy: Map<string, boolean>;
40
49
  constructor(options: IDeployerServiceOptions);
@@ -107,3 +116,4 @@ export declare class DeployerService extends BaseService {
107
116
  unpack(destination: string, uploadedFile: string, install?: boolean, ignoreDirectoryClean?: boolean): Promise<IError | null>;
108
117
  upload(file: UploadedFile, uploadPath: string): Promise<IError | null>;
109
118
  }
119
+ export {};
@@ -12,16 +12,29 @@ const LogService_1 = require("../LogService");
12
12
  const __1 = require("../..");
13
13
  const Notifier_1 = require("./Notifier");
14
14
  const child_process_1 = require("child_process");
15
- const util_1 = require("util");
16
15
  const fs_1 = __importDefault(require("fs"));
17
16
  const crypto_1 = __importDefault(require("crypto"));
18
17
  const Watchdog_1 = require("./Watchdog");
19
- const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
18
+ const NginxHelper_1 = require("./NginxHelper");
19
+ const _TSetup = {
20
+ users: [{
21
+ login: "",
22
+ password: ""
23
+ }],
24
+ watchdog: true,
25
+ $__watchdog_optional: true,
26
+ notifier: {
27
+ URL: "",
28
+ KEY: "",
29
+ },
30
+ $__notifier_optional: true
31
+ };
20
32
  exports.REQ_DEPLOYMENT_PROCEED = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_SERVICE");
21
33
  exports.REQ_DEPLOYMENT_SWITCH = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_SWITCH");
22
34
  exports.REQ_DEPLOYMENT_STATUS = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_STATUS");
23
35
  class DeployerService extends BaseService_1.BaseService {
24
36
  options;
37
+ setup = null;
25
38
  notifier = null;
26
39
  busy = new Map();
27
40
  constructor(options) {
@@ -32,20 +45,35 @@ class DeployerService extends BaseService_1.BaseService {
32
45
  super.init();
33
46
  const cs = new ConfigService_1.ConfigService(this.options.projects_path);
34
47
  await cs.init();
48
+ const setupFile = path_1.default.resolve(this.options.setup_path, "setup.json");
49
+ if (!fs_1.default.existsSync(setupFile)) {
50
+ (0, LogService_1.logError)("Setup file not found: " + setupFile);
51
+ throw new Error("Setup file not found: " + setupFile);
52
+ }
53
+ try {
54
+ this.setup = JSON.parse(fs_1.default.readFileSync(setupFile, "utf-8"));
55
+ }
56
+ catch (e) {
57
+ (0, LogService_1.logError)("Failed to read setup file: " + e.message);
58
+ throw new Error("Failed to read setup file: " + e.message);
59
+ }
60
+ const validation = await __1.Validator.validateStructure(_TSetup, this.setup);
61
+ if (validation && validation.length > 0) {
62
+ (0, LogService_1.logError)("Setup file validation failed: " + JSON.stringify(validation));
63
+ throw new Error("Setup file validation failed: " + JSON.stringify(validation));
64
+ }
65
+ if (!this.setup) {
66
+ (0, LogService_1.logError)("Setup file is empty or invalid: " + setupFile);
67
+ throw new Error("Setup file is empty or invalid: " + setupFile);
68
+ }
35
69
  let notifier = null;
36
- if (this.options.notifier) {
37
- if (this.options.notifier.controller) {
38
- notifier = this.options.notifier.controller;
39
- notifier.init();
40
- }
41
- else {
42
- if (this.options.notifier.URL && this.options.notifier.KEY) {
43
- notifier = new Notifier_1.Notifier({ URL: this.options.notifier.URL, KEY: this.options.notifier.KEY });
44
- await notifier.init();
45
- }
70
+ if (this.setup && this.setup.notifier) {
71
+ if (this.setup.notifier.URL && this.setup.notifier.KEY) {
72
+ notifier = new Notifier_1.Notifier({ URL: this.setup.notifier.URL, KEY: this.setup.notifier.KEY });
73
+ await notifier.init();
46
74
  }
47
75
  }
48
- if (this.options.watchdog) {
76
+ if (this.setup && this.setup.watchdog) {
49
77
  const wd = new Watchdog_1.Watchdog(notifier);
50
78
  await wd.init();
51
79
  }
@@ -112,11 +140,12 @@ class DeployerService extends BaseService_1.BaseService {
112
140
  fs_1.default.mkdirSync(deployPath, { recursive: true });
113
141
  found.destination = deployPath;
114
142
  if (projectState && found.bluegreen) {
143
+ const baseConfig = found.config ?? {};
115
144
  if (projectState.lastDeployed === "blue" && found.bluegreen.blue_config) {
116
- found.config = found.bluegreen.blue_config;
145
+ found.config = { ...baseConfig, ...found.bluegreen.blue_config };
117
146
  }
118
147
  else if (projectState.lastDeployed === "green" && found.bluegreen.green_config) {
119
- found.config = found.bluegreen.green_config;
148
+ found.config = { ...baseConfig, ...found.bluegreen.green_config };
120
149
  }
121
150
  }
122
151
  found.name = data.name + "-" + projectState.lastDeployed;
@@ -157,6 +186,28 @@ class DeployerService extends BaseService_1.BaseService {
157
186
  if (found.config) {
158
187
  await this.applyConfig(found.destination, found.config);
159
188
  }
189
+ if (found.nginx?.config) {
190
+ const nginxPath = path_1.default.resolve(found.destination, found.nginx.config);
191
+ if (!fs_1.default.existsSync(nginxPath)) {
192
+ if (found.email && found.email.length > 0) {
193
+ for (let email of found.email) {
194
+ this.notifier?.notifyEmailStatus(email, "error-nginx-missing", found.name, user.login);
195
+ }
196
+ }
197
+ throw { code: 22, message: "nginx config declared but file not found: " + nginxPath, httpStatus: 500 };
198
+ }
199
+ try {
200
+ NginxHelper_1.NginxHelper.applyTemplate(nginxPath, found.config ?? {});
201
+ }
202
+ catch (e) {
203
+ if (found.email && found.email.length > 0) {
204
+ for (let email of found.email) {
205
+ this.notifier?.notifyEmailStatus(email, "error-nginx-render", found.name, user.login);
206
+ }
207
+ }
208
+ throw { code: 23, message: "nginx template render failed: " + String(e?.message ?? e), httpStatus: 500 };
209
+ }
210
+ }
160
211
  if (found.pm2) {
161
212
  const pm2Result = await this.runPM2(found, projectState);
162
213
  if (__1.ErrorUtils.isError(pm2Result)) {
@@ -262,31 +313,14 @@ class DeployerService extends BaseService_1.BaseService {
262
313
  fs_1.default.writeFileSync(tmp, JSON.stringify(projectState, null, 4), "utf-8");
263
314
  fs_1.default.renameSync(tmp, projectStatePath);
264
315
  let hookError = null;
265
- const rawHook = found.bluegreen.on_switch;
266
- if (Array.isArray(rawHook)) {
267
- const cmd = rawHook.filter(s => typeof s === "string" && s.length > 0);
268
- if (cmd.length > 0) {
269
- if (!path_1.default.isAbsolute(cmd[0])) {
270
- hookError = "on_switch command must be an absolute path, got: " + cmd[0];
271
- (0, LogService_1.logError)(hookError);
272
- }
273
- else {
274
- try {
275
- const result = await execFileAsync(cmd[0], cmd.slice(1), {
276
- timeout: 30_000,
277
- maxBuffer: 4 * 1024 * 1024,
278
- env: { PATH: process.env.PATH ?? "/usr/bin:/bin" },
279
- });
280
- if (result.stderr)
281
- (0, LogService_1.logWarn)("on_switch stderr:", result.stderr);
282
- }
283
- catch (e) {
284
- hookError = String(e?.stderr || e?.message || e);
285
- (0, LogService_1.logError)("on_switch hook failed:", hookError);
286
- }
287
- }
316
+ if (found.nginx?.config && found.nginx?.symlink) {
317
+ const symlinkTarget = path_1.default.resolve(found.destination, target, found.nginx.config);
318
+ const swapErr = NginxHelper_1.NginxHelper.swapSymlink(found.nginx.symlink, symlinkTarget);
319
+ if (swapErr) {
320
+ return { error: swapErr };
288
321
  }
289
322
  }
323
+ hookError = await NginxHelper_1.NginxHelper.runHook(found.bluegreen.on_switch);
290
324
  const status = hookError ? "switch-hook-failed" : action === "rollback" ? `rolled-back-to-${target}` : `switched-to-${target}`;
291
325
  if (found.email && found.email.length > 0) {
292
326
  for (const email of found.email) {
@@ -338,13 +372,15 @@ class DeployerService extends BaseService_1.BaseService {
338
372
  _checkUserAuth(authHeader) {
339
373
  if (!authHeader)
340
374
  return null;
375
+ if (!this.setup)
376
+ return null;
341
377
  const token = authHeader.split(" ").pop() ?? "";
342
378
  if (!token)
343
379
  return null;
344
- if (!this.options.users || this.options.users.length === 0)
380
+ if (!this.setup.users || this.setup.users.length === 0)
345
381
  return null;
346
382
  let matched = null;
347
- for (const user of this.options.users) {
383
+ for (const user of this.setup.users) {
348
384
  const expected = crypto_1.default.createHash("sha256").update(user.login + ":" + user.password).digest("hex");
349
385
  if (this.tokensMatch(token, expected))
350
386
  matched = user;
@@ -1,2 +1,9 @@
1
1
  export declare class NginxHelper {
2
+ static applyTemplate(filePath: string, values: Record<string, string>): void;
3
+ static swapSymlink(symlinkPath: string, target: string): {
4
+ code: number;
5
+ message: string;
6
+ httpStatus: number;
7
+ } | null;
8
+ static runHook(cmd: string[] | undefined | null): Promise<string | null>;
2
9
  }
@@ -1,37 +1,80 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.NginxHelper = void 0;
4
- const default_conf = `
5
- server {
6
- server_name paypartner.eu www.paypartner.eu apgp.paypartner.eu;
7
- root /var/www/apgp.paypartner.eu;
8
- index index.html;
9
-
10
- location /api {
11
- include /etc/nginx/proxy_common.conf;
12
- proxy_set_header X-Branch paypartner;
13
- #paysolo api, 8096 to paypartner api
14
- # 8065 -new paysolo
15
- # 8095 - old paysolo
16
- proxy_pass http://127.0.0.1:8065;
17
- }
18
-
19
-
20
- location / {
21
- try_files $uri $uri/ /index.html;
22
- }
23
-
24
- listen 443 ssl; # managed by Certbot
25
- #ssl_certificate /etc/letsencrypt/live/apgp.paypartner.eu/fullchain.pem; # managed by Certbot
26
- #ssl_certificate_key /etc/letsencrypt/live/apgp.paypartner.eu/privkey.pem; # managed by Certbot
27
-
28
- ssl_certificate /etc/letsencrypt/live/paypartner.eu/fullchain.pem;
29
- ssl_certificate_key /etc/letsencrypt/live/paypartner.eu/privkey.pem;
30
-
31
- include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
32
- ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
33
- }
34
- `;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
10
+ const util_1 = require("util");
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ const LogService_1 = require("../LogService");
13
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
35
14
  class NginxHelper {
15
+ static applyTemplate(filePath, values) {
16
+ if (!fs_1.default.existsSync(filePath))
17
+ throw new Error("nginx config file not found: " + filePath);
18
+ const original = fs_1.default.readFileSync(filePath, "utf-8");
19
+ const hashBefore = crypto_1.default.createHash("md5").update(original).digest("hex");
20
+ let content = original;
21
+ for (const [key, value] of Object.entries(values)) {
22
+ content = content.replaceAll(`_{{${key}}}_`, value);
23
+ }
24
+ const hashAfter = crypto_1.default.createHash("md5").update(content).digest("hex");
25
+ if (hashBefore !== hashAfter) {
26
+ fs_1.default.writeFileSync(filePath, content, "utf-8");
27
+ }
28
+ const leftover = content.match(/_\{\{[A-Za-z0-9_]+\}\}_/g);
29
+ if (leftover && leftover.length > 0) {
30
+ const unique = [...new Set(leftover)];
31
+ (0, LogService_1.logWarn)("Unresolved template placeholders in nginx config " + filePath + ": " + unique.join(", "));
32
+ }
33
+ }
34
+ static swapSymlink(symlinkPath, target) {
35
+ if (!fs_1.default.existsSync(target)) {
36
+ return { code: 48, message: "nginx symlink target missing: " + target, httpStatus: 500 };
37
+ }
38
+ try {
39
+ const tmp = symlinkPath + ".tmp";
40
+ try {
41
+ fs_1.default.unlinkSync(tmp);
42
+ }
43
+ catch { }
44
+ fs_1.default.symlinkSync(target, tmp);
45
+ fs_1.default.renameSync(tmp, symlinkPath);
46
+ return null;
47
+ }
48
+ catch (e) {
49
+ return { code: 49, message: "nginx symlink swap failed: " + String(e?.message ?? e), httpStatus: 500 };
50
+ }
51
+ }
52
+ static async runHook(cmd) {
53
+ if (!Array.isArray(cmd))
54
+ return null;
55
+ const filtered = cmd.filter(s => typeof s === "string" && s.length > 0);
56
+ if (filtered.length === 0)
57
+ return null;
58
+ if (!path_1.default.isAbsolute(filtered[0])) {
59
+ const err = "nginx hook command must be an absolute path, got: " + filtered[0];
60
+ (0, LogService_1.logError)(err);
61
+ return err;
62
+ }
63
+ try {
64
+ const result = await execFileAsync(filtered[0], filtered.slice(1), {
65
+ timeout: 30_000,
66
+ maxBuffer: 4 * 1024 * 1024,
67
+ env: { PATH: process.env.PATH ?? "/usr/bin:/bin" },
68
+ });
69
+ if (result.stderr)
70
+ (0, LogService_1.logWarn)("nginx hook stderr:", result.stderr);
71
+ return null;
72
+ }
73
+ catch (e) {
74
+ const err = String(e?.stderr || e?.message || e);
75
+ (0, LogService_1.logError)("nginx hook failed:", err);
76
+ return err;
77
+ }
78
+ }
36
79
  }
37
80
  exports.NginxHelper = NginxHelper;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "badmfck-api-server",
3
- "version": "4.1.29",
3
+ "version": "4.1.31",
4
4
  "description": "Simple API http server based on express",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",