badmfck-api-server 4.1.27 → 4.1.29

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.
@@ -0,0 +1,543 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DeployerService = exports.REQ_DEPLOYMENT_STATUS = exports.REQ_DEPLOYMENT_SWITCH = exports.REQ_DEPLOYMENT_PROCEED = void 0;
7
+ const badmfck_signal_1 = require("badmfck-signal");
8
+ const BaseService_1 = require("../BaseService");
9
+ const ConfigService_1 = require("./ConfigService");
10
+ const path_1 = __importDefault(require("path"));
11
+ const LogService_1 = require("../LogService");
12
+ const __1 = require("../..");
13
+ const Notifier_1 = require("./Notifier");
14
+ const child_process_1 = require("child_process");
15
+ const util_1 = require("util");
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const crypto_1 = __importDefault(require("crypto"));
18
+ const Watchdog_1 = require("./Watchdog");
19
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
20
+ exports.REQ_DEPLOYMENT_PROCEED = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_SERVICE");
21
+ exports.REQ_DEPLOYMENT_SWITCH = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_SWITCH");
22
+ exports.REQ_DEPLOYMENT_STATUS = new badmfck_signal_1.Req(undefined, "REQ_DEPLOYER_STATUS");
23
+ class DeployerService extends BaseService_1.BaseService {
24
+ options;
25
+ notifier = null;
26
+ busy = new Map();
27
+ constructor(options) {
28
+ super("DeployerService");
29
+ this.options = options;
30
+ }
31
+ async init() {
32
+ super.init();
33
+ const cs = new ConfigService_1.ConfigService(this.options.projects_path);
34
+ await cs.init();
35
+ 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
+ }
46
+ }
47
+ }
48
+ if (this.options.watchdog) {
49
+ const wd = new Watchdog_1.Watchdog(notifier);
50
+ await wd.init();
51
+ }
52
+ exports.REQ_DEPLOYMENT_PROCEED.listener = async (data) => this.proceed(data);
53
+ exports.REQ_DEPLOYMENT_SWITCH.listener = async (data) => this.switchSlot(data);
54
+ exports.REQ_DEPLOYMENT_STATUS.listener = async (data) => this.status(data);
55
+ }
56
+ async proceed(data) {
57
+ const user = this._checkUserAuth(data.auth);
58
+ if (!user) {
59
+ (0, LogService_1.logError)("Deploy rejected: user auth failed");
60
+ return { error: { code: 20, message: "Unauthorized.", httpStatus: 401 } };
61
+ }
62
+ const configurations = await ConfigService_1.REQ_DEPLOYMENT_CONFIG.request();
63
+ let found;
64
+ for (let i of configurations.values()) {
65
+ if (i.name === data.name && this.tokensMatch(i.token, data.token)) {
66
+ found = i;
67
+ break;
68
+ }
69
+ }
70
+ if (!found) {
71
+ (0, LogService_1.logError)("Deploy rejected: wrong token and/or name");
72
+ return { error: { code: 12, message: "Wrong token and/or name.", httpStatus: 400 } };
73
+ }
74
+ if (this.busy.get(found.name))
75
+ return { error: { code: 13, message: "Deployment already in progress.", httpStatus: 400 } };
76
+ const realName = found.name;
77
+ this.busy.set(realName, true);
78
+ try {
79
+ found = structuredClone(found);
80
+ const file = data.files.file;
81
+ const safeName = (path_1.default.basename(file.name || "").replace(/[^a-zA-Z0-9._-]/g, "_")) || "upload";
82
+ let projectState = null;
83
+ let projectStatePath = null;
84
+ if (found.bluegreen && found.bluegreen.blue_config && found.bluegreen.green_config) {
85
+ projectStatePath = path_1.default.resolve(this.options.projects_path, data.name + "_state_.json");
86
+ if (fs_1.default.existsSync(projectStatePath)) {
87
+ try {
88
+ projectState = JSON.parse(fs_1.default.readFileSync(projectStatePath, "utf-8"));
89
+ }
90
+ catch (e) {
91
+ (0, LogService_1.logError)("Failed to load project state: " + e.message);
92
+ }
93
+ }
94
+ if (!projectState) {
95
+ projectState = {
96
+ active: "blue",
97
+ lastDeployed: "blue",
98
+ history: []
99
+ };
100
+ }
101
+ else {
102
+ projectState.lastDeployed = projectState.active === "blue" ? "green" : "blue";
103
+ }
104
+ projectState.history.push({
105
+ slot: projectState.lastDeployed,
106
+ action: "deploy",
107
+ user: user.login,
108
+ at: Date.now()
109
+ });
110
+ const subdir = projectState.lastDeployed;
111
+ const deployPath = path_1.default.resolve(found.destination, subdir);
112
+ fs_1.default.mkdirSync(deployPath, { recursive: true });
113
+ found.destination = deployPath;
114
+ if (projectState && found.bluegreen) {
115
+ if (projectState.lastDeployed === "blue" && found.bluegreen.blue_config) {
116
+ found.config = found.bluegreen.blue_config;
117
+ }
118
+ else if (projectState.lastDeployed === "green" && found.bluegreen.green_config) {
119
+ found.config = found.bluegreen.green_config;
120
+ }
121
+ }
122
+ found.name = data.name + "-" + projectState.lastDeployed;
123
+ }
124
+ const uploadPath = path_1.default.resolve(found.destination, __1.UID.generate() + "_" + safeName);
125
+ const result = await this.upload(file, uploadPath);
126
+ if (__1.ErrorUtils.isError(result)) {
127
+ if (found.email && found.email.length > 0) {
128
+ for (let email of found.email) {
129
+ this.notifier?.notifyEmailStatus(email, "error-copy", found.name, user.login);
130
+ }
131
+ }
132
+ throw result;
133
+ }
134
+ if (found.backup) {
135
+ console.log("TODO: backup");
136
+ }
137
+ if (found.unpack) {
138
+ const unpackResult = await this.unpack(found.destination, uploadPath, found.install, found.ignoreDirectoryClean);
139
+ if (__1.ErrorUtils.isError(unpackResult)) {
140
+ if (found.email && found.email.length > 0) {
141
+ for (let email of found.email) {
142
+ this.notifier?.notifyEmailStatus(email, "error-unpack", found.name, user.login);
143
+ }
144
+ }
145
+ throw unpackResult;
146
+ }
147
+ }
148
+ if (found.chmod && found.chmod > 1) {
149
+ try {
150
+ console.log("Try to execute: ", `chmod -R ${found.chmod} ${found.destination}`);
151
+ (0, child_process_1.execFileSync)("chmod", ["-R", String(found.chmod), found.destination]);
152
+ }
153
+ catch (e) {
154
+ (0, LogService_1.logError)("Can't execute chmod", e);
155
+ }
156
+ }
157
+ if (found.config) {
158
+ await this.applyConfig(found.destination, found.config);
159
+ }
160
+ if (found.pm2) {
161
+ const pm2Result = await this.runPM2(found, projectState);
162
+ if (__1.ErrorUtils.isError(pm2Result)) {
163
+ if (found.email && found.email.length > 0) {
164
+ for (let email of found.email) {
165
+ this.notifier?.notifyEmailStatus(email, "error-pm2", found.name, user.login);
166
+ }
167
+ }
168
+ throw pm2Result;
169
+ }
170
+ }
171
+ if (projectState && projectStatePath) {
172
+ const tmp = projectStatePath + ".tmp";
173
+ fs_1.default.writeFileSync(tmp, JSON.stringify(projectState, null, 4), "utf-8");
174
+ fs_1.default.renameSync(tmp, projectStatePath);
175
+ }
176
+ if (found.email && found.email.length > 0) {
177
+ for (let email of found.email) {
178
+ this.notifier?.notifyEmailStatus(email, "done", found.name, user.login);
179
+ }
180
+ }
181
+ if (found.phone && found.phone.length > 0) {
182
+ for (let p of found.phone) {
183
+ this.notifier?.notifySMSStatus(p, found.name + " deployment done", user.login);
184
+ }
185
+ }
186
+ if (found.liveness) {
187
+ const conf = found;
188
+ const liveness = found.liveness;
189
+ setTimeout(async () => {
190
+ const result = await __1.Http.get(liveness);
191
+ if (!result.ok) {
192
+ (0, LogService_1.logError)("Liveness check failed for", conf.name);
193
+ if (conf.email && conf.email.length > 0) {
194
+ for (let email of conf.email) {
195
+ this.notifier?.notifyEmailStatus(email, "error-liveness", conf.name, user.login);
196
+ }
197
+ }
198
+ }
199
+ }, 5000);
200
+ }
201
+ return { data: true };
202
+ }
203
+ finally {
204
+ this.busy.set(realName, false);
205
+ }
206
+ }
207
+ async switchSlot(data) {
208
+ const user = this._checkUserAuth(data.auth);
209
+ if (!user) {
210
+ (0, LogService_1.logError)("Switch rejected: user auth failed");
211
+ return { error: { code: 40, message: "Unauthorized.", httpStatus: 401 } };
212
+ }
213
+ const configurations = await ConfigService_1.REQ_DEPLOYMENT_CONFIG.request();
214
+ let found;
215
+ for (const i of configurations.values()) {
216
+ if (i.name === data.name && this.tokensMatch(i.token, data.token)) {
217
+ found = i;
218
+ break;
219
+ }
220
+ }
221
+ if (!found) {
222
+ (0, LogService_1.logError)("Switch rejected: wrong token and/or name");
223
+ return { error: { code: 41, message: "Wrong token and/or name.", httpStatus: 400 } };
224
+ }
225
+ if (!found.bluegreen || !found.bluegreen.blue_config || !found.bluegreen.green_config) {
226
+ return { error: { code: 42, message: "Project has no blue-green configuration.", httpStatus: 400 } };
227
+ }
228
+ if (this.busy.get(found.name)) {
229
+ return { error: { code: 43, message: "Deployment in progress, try again shortly.", httpStatus: 409 } };
230
+ }
231
+ const realName = found.name;
232
+ this.busy.set(realName, true);
233
+ try {
234
+ const projectStatePath = path_1.default.resolve(this.options.projects_path, data.name + "_state_.json");
235
+ if (!fs_1.default.existsSync(projectStatePath)) {
236
+ return { error: { code: 44, message: "Project has never been deployed.", httpStatus: 400 } };
237
+ }
238
+ let projectState;
239
+ try {
240
+ projectState = JSON.parse(fs_1.default.readFileSync(projectStatePath, "utf-8"));
241
+ }
242
+ catch (e) {
243
+ return { error: { code: 45, message: "Corrupt state file: " + e.message, httpStatus: 500 } };
244
+ }
245
+ const target = data.scheme ?? projectState.lastDeployed;
246
+ if (target !== "blue" && target !== "green") {
247
+ return { error: { code: 46, message: "Invalid target slot.", httpStatus: 400 } };
248
+ }
249
+ if (target === projectState.active) {
250
+ return { error: { code: 47, message: `Slot '${target}' is already active.`, httpStatus: 400 } };
251
+ }
252
+ const previousActive = projectState.active;
253
+ const action = target === projectState.lastDeployed ? "switch" : "rollback";
254
+ projectState.active = target;
255
+ projectState.history.push({
256
+ slot: target,
257
+ action,
258
+ user: user.login,
259
+ at: Date.now(),
260
+ });
261
+ const tmp = projectStatePath + ".tmp";
262
+ fs_1.default.writeFileSync(tmp, JSON.stringify(projectState, null, 4), "utf-8");
263
+ fs_1.default.renameSync(tmp, projectStatePath);
264
+ 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
+ }
288
+ }
289
+ }
290
+ const status = hookError ? "switch-hook-failed" : action === "rollback" ? `rolled-back-to-${target}` : `switched-to-${target}`;
291
+ if (found.email && found.email.length > 0) {
292
+ for (const email of found.email) {
293
+ this.notifier?.notifyEmailStatus(email, status, found.name, user.login);
294
+ }
295
+ }
296
+ if (found.phone && found.phone.length > 0) {
297
+ for (const p of found.phone) {
298
+ this.notifier?.notifySMSStatus(p, `${found.name} ${action} to ${target}`, user.login);
299
+ }
300
+ }
301
+ return { data: { active: projectState.active, previousActive, action, hookError } };
302
+ }
303
+ finally {
304
+ this.busy.set(realName, false);
305
+ }
306
+ }
307
+ async status(data) {
308
+ const user = this._checkUserAuth(data.auth);
309
+ if (!user) {
310
+ return { error: { code: 50, message: "Unauthorized.", httpStatus: 401 } };
311
+ }
312
+ const configurations = await ConfigService_1.REQ_DEPLOYMENT_CONFIG.request();
313
+ const results = {};
314
+ for (const cfg of configurations.values()) {
315
+ if (data.name && cfg.name !== data.name)
316
+ continue;
317
+ const statePath = path_1.default.resolve(this.options.projects_path, cfg.name + "_state_.json");
318
+ let state = null;
319
+ if (fs_1.default.existsSync(statePath)) {
320
+ try {
321
+ state = JSON.parse(fs_1.default.readFileSync(statePath, "utf-8"));
322
+ }
323
+ catch { }
324
+ }
325
+ results[cfg.name] = {
326
+ hasBlueGreen: !!(cfg.bluegreen && cfg.bluegreen.blue_config && cfg.bluegreen.green_config),
327
+ state,
328
+ readyToSwitch: !!(state && state.lastDeployed !== state.active),
329
+ };
330
+ }
331
+ return { data: results };
332
+ }
333
+ tokensMatch(a, b) {
334
+ const ha = new Uint8Array(crypto_1.default.createHash("sha256").update(String(a ?? "")).digest());
335
+ const hb = new Uint8Array(crypto_1.default.createHash("sha256").update(String(b ?? "")).digest());
336
+ return crypto_1.default.timingSafeEqual(ha, hb);
337
+ }
338
+ _checkUserAuth(authHeader) {
339
+ if (!authHeader)
340
+ return null;
341
+ const token = authHeader.split(" ").pop() ?? "";
342
+ if (!token)
343
+ return null;
344
+ if (!this.options.users || this.options.users.length === 0)
345
+ return null;
346
+ let matched = null;
347
+ for (const user of this.options.users) {
348
+ const expected = crypto_1.default.createHash("sha256").update(user.login + ":" + user.password).digest("hex");
349
+ if (this.tokensMatch(token, expected))
350
+ matched = user;
351
+ }
352
+ return matched;
353
+ }
354
+ async runPM2(found, projectState) {
355
+ let pm2attrs = projectState?.lastDeployed === "blue" ? found.bluegreen?.blue_pm2_attributes : found.bluegreen?.green_pm2_attributes;
356
+ const pm2Attrs = Array.isArray(pm2attrs) ? pm2attrs.filter(s => typeof s === "string" && s.length > 0) : found.pm2_attributes;
357
+ return new Promise((resolve) => {
358
+ try {
359
+ (0, child_process_1.execFileSync)("pm2", ["--version"]);
360
+ const extra = Array.isArray(pm2Attrs)
361
+ ? pm2Attrs.filter(s => typeof s === "string" && s.length > 0)
362
+ : [];
363
+ const ecoJs = path_1.default.resolve(found.destination, "ecosystem.config.js");
364
+ const ecoCjs = path_1.default.resolve(found.destination, "ecosystem.config.cjs");
365
+ const ecosystem = fs_1.default.existsSync(ecoJs) ? ecoJs
366
+ : fs_1.default.existsSync(ecoCjs) ? ecoCjs
367
+ : null;
368
+ if (ecosystem) {
369
+ (0, child_process_1.execFileSync)("pm2", ["startOrReload", ecosystem, "--update-env", ...extra]);
370
+ resolve(null);
371
+ return;
372
+ }
373
+ const list = (0, child_process_1.execFileSync)("pm2", ["list"]).toString();
374
+ let running = false;
375
+ for (let l of list.split("\n")) {
376
+ if (l.indexOf(found.name) !== -1) {
377
+ running = true;
378
+ break;
379
+ }
380
+ }
381
+ const packageJson = JSON.parse(fs_1.default.readFileSync(path_1.default.resolve(found.destination, "package.json")).toString());
382
+ let main = packageJson.main || "index.js";
383
+ if (main.startsWith("./"))
384
+ main = main.substring(2);
385
+ if (!found.destination.endsWith("/"))
386
+ main = "/" + main;
387
+ if (running) {
388
+ try {
389
+ (0, child_process_1.execFileSync)("pm2", ["delete", found.name]);
390
+ }
391
+ catch { }
392
+ }
393
+ (0, child_process_1.execFileSync)("pm2", ["start", found.destination + main, "--name", found.name, ...extra]);
394
+ resolve(null);
395
+ }
396
+ catch (e) {
397
+ resolve({ code: 18, message: "Can't run pm2", details: "" + e, httpStatus: 500 });
398
+ }
399
+ });
400
+ }
401
+ static SKIP_DIRS = new Set(["node_modules", ".git", "__tmp"]);
402
+ static TEMPLATE_EXTENSIONS = [".js", ".ts", ".json", ".env"];
403
+ async applyConfig(dir, config) {
404
+ if (Object.keys(config).length < 1)
405
+ return;
406
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
407
+ for (const entry of entries) {
408
+ if (entry.isSymbolicLink())
409
+ continue;
410
+ if (entry.isDirectory()) {
411
+ if (DeployerService.SKIP_DIRS.has(entry.name))
412
+ continue;
413
+ const fullPath = path_1.default.join(dir, entry.name);
414
+ await this.applyConfig(fullPath, config);
415
+ continue;
416
+ }
417
+ if (entry.isFile()) {
418
+ const fullPath = path_1.default.join(dir, entry.name);
419
+ const lower = fullPath.toLowerCase();
420
+ if (DeployerService.TEMPLATE_EXTENSIONS.some(ext => lower.endsWith(ext)))
421
+ await this.processFile(fullPath, config);
422
+ }
423
+ }
424
+ }
425
+ async processFile(filePath, config) {
426
+ let content = fs_1.default.readFileSync(filePath, "utf-8");
427
+ let hash = crypto_1.default.createHash("md5").update(content).digest("hex");
428
+ for (const [key, value] of Object.entries(config)) {
429
+ content = content.replaceAll(`_{{${key}}}_`, value);
430
+ }
431
+ let hash2 = crypto_1.default.createHash("md5").update(content).digest("hex");
432
+ if (hash !== hash2) {
433
+ console.log("Values was changed in " + filePath);
434
+ fs_1.default.writeFileSync(filePath, content, "utf-8");
435
+ }
436
+ const leftover = content.match(/_\{\{[A-Za-z0-9_]+\}\}_/g);
437
+ if (leftover && leftover.length > 0) {
438
+ const unique = [...new Set(leftover)];
439
+ (0, LogService_1.logWarn)("Unresolved template placeholders in " + filePath + ": " + unique.join(", "));
440
+ }
441
+ }
442
+ _isDestinationSafe(destination) {
443
+ try {
444
+ const dest = path_1.default.resolve(destination);
445
+ const root = path_1.default.resolve(this.options.projects_path);
446
+ if (!path_1.default.isAbsolute(dest))
447
+ return false;
448
+ if (dest.length < 5 || dest === "/")
449
+ return false;
450
+ if (dest === root)
451
+ return false;
452
+ if (!dest.startsWith(root + path_1.default.sep))
453
+ return false;
454
+ return true;
455
+ }
456
+ catch {
457
+ return false;
458
+ }
459
+ }
460
+ async unpack(destination, uploadedFile, install, ignoreDirectoryClean) {
461
+ return new Promise((resolve) => {
462
+ if (!this._isDestinationSafe(destination)) {
463
+ (0, LogService_1.logError)("Refusing to unpack: destination outside projects_path", destination);
464
+ resolve({ code: 19, message: "Destination outside projects root", httpStatus: 500 });
465
+ return;
466
+ }
467
+ try {
468
+ const tmp = path_1.default.resolve(destination, "__tmp");
469
+ if (fs_1.default.existsSync(tmp))
470
+ fs_1.default.rmSync(tmp, { recursive: true });
471
+ if (!fs_1.default.existsSync(tmp))
472
+ fs_1.default.mkdirSync(tmp, { recursive: true });
473
+ (0, child_process_1.execFileSync)("tar", ["--no-absolute-names", "-xzf", uploadedFile, "-C", tmp]);
474
+ }
475
+ catch (e) {
476
+ (0, LogService_1.logError)(e);
477
+ try {
478
+ fs_1.default.rmSync(path_1.default.resolve(uploadedFile));
479
+ }
480
+ catch (e) { }
481
+ resolve({ code: 14, message: "Can't unpack file", httpStatus: 500 });
482
+ return;
483
+ }
484
+ try {
485
+ fs_1.default.rmSync(path_1.default.resolve(uploadedFile));
486
+ }
487
+ catch (e) {
488
+ (0, LogService_1.logError)("Can't remove file", e);
489
+ }
490
+ if (install) {
491
+ try {
492
+ (0, child_process_1.execFileSync)("npm", ["--prefix", path_1.default.resolve(destination, "__tmp"), "i", "--ignore-scripts"]);
493
+ }
494
+ catch (e) {
495
+ console.error(e);
496
+ (0, LogService_1.logError)(e);
497
+ resolve({ code: 17, message: "Can't install", httpStatus: 500 });
498
+ return;
499
+ }
500
+ }
501
+ if (!ignoreDirectoryClean) {
502
+ fs_1.default.readdirSync(destination).forEach((file) => {
503
+ if (file === "__tmp")
504
+ return;
505
+ try {
506
+ fs_1.default.rmSync(path_1.default.resolve(destination, file), { recursive: true });
507
+ }
508
+ catch (e) {
509
+ (0, LogService_1.logError)(e);
510
+ }
511
+ });
512
+ }
513
+ try {
514
+ (0, child_process_1.execFileSync)("cp", ["-rf", path_1.default.resolve(destination, "__tmp") + "/.", destination]);
515
+ }
516
+ catch (e) {
517
+ (0, LogService_1.logError)(e);
518
+ resolve({ code: 15, message: "Can't copy files", httpStatus: 500 });
519
+ return;
520
+ }
521
+ try {
522
+ fs_1.default.rmSync(path_1.default.resolve(destination, "__tmp"), { recursive: true });
523
+ }
524
+ catch (e) {
525
+ (0, LogService_1.logError)(e);
526
+ resolve({ code: 16, message: "Can't remove tmp", httpStatus: 500 });
527
+ }
528
+ resolve(null);
529
+ });
530
+ }
531
+ async upload(file, uploadPath) {
532
+ return new Promise((resolve) => {
533
+ file.mv(uploadPath, (err) => {
534
+ if (err) {
535
+ resolve({ code: 13, message: "Can't upload file", httpStatus: 500, details: err });
536
+ return;
537
+ }
538
+ resolve(null);
539
+ });
540
+ });
541
+ }
542
+ }
543
+ exports.DeployerService = DeployerService;
@@ -0,0 +1,2 @@
1
+ export declare class NginxHelper {
2
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ 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
+ `;
35
+ class NginxHelper {
36
+ }
37
+ exports.NginxHelper = NginxHelper;
@@ -0,0 +1,16 @@
1
+ import { BaseService } from '../BaseService';
2
+ export interface IDeploymentNotifier extends BaseService {
3
+ notifyEmailStatus(email: string, status: string, project: string, user: string): Promise<void>;
4
+ notifySMSStatus(phone: string, text: string, user: string): Promise<void>;
5
+ }
6
+ export declare class Notifier extends BaseService implements IDeploymentNotifier {
7
+ SMS_URL: string;
8
+ SMS_KEY: string;
9
+ constructor({ URL, KEY }: {
10
+ URL: string;
11
+ KEY: string;
12
+ });
13
+ init(): Promise<void>;
14
+ notifyEmailStatus(email: string, status: string, project: string, user: string): Promise<void>;
15
+ notifySMSStatus(phone: string, text: string, user: string): Promise<void>;
16
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Notifier = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const BaseService_1 = require("../BaseService");
6
+ const __1 = require("../..");
7
+ class Notifier extends BaseService_1.BaseService {
8
+ SMS_URL;
9
+ SMS_KEY;
10
+ constructor({ URL, KEY }) {
11
+ super("Notifier");
12
+ this.SMS_URL = URL;
13
+ this.SMS_KEY = KEY;
14
+ }
15
+ async init() {
16
+ super.init();
17
+ }
18
+ async notifyEmailStatus(email, status, project, user) {
19
+ const text = `${project} deployment <b>${status}</b> by <b>${user}</b> ${new Date().toISOString()}`;
20
+ const title = `Deployment: ${project} - ${status} (${user})`;
21
+ const token = (0, crypto_1.createHash)("sha256").update(this.SMS_KEY + email + title + text).digest("base64");
22
+ let response = null;
23
+ try {
24
+ response = await __1.Http.post(this.SMS_URL + "/api/mail/send", {
25
+ to: email,
26
+ text: title,
27
+ html: text
28
+ }, {
29
+ headers: {
30
+ "authorization": token,
31
+ "Content-Type": "application/json"
32
+ }
33
+ });
34
+ }
35
+ catch (e) {
36
+ console.error(e);
37
+ }
38
+ }
39
+ async notifySMSStatus(phone, text, user) {
40
+ const finalText = `${text} (by ${user})`;
41
+ const token = (0, crypto_1.createHash)("sha256").update(this.SMS_KEY + phone + finalText + "").digest("base64");
42
+ let response = null;
43
+ try {
44
+ response = await __1.Http.post(this.SMS_URL + "/api/sms", {
45
+ to: phone,
46
+ content: finalText
47
+ }, {
48
+ headers: {
49
+ "authorization": token,
50
+ "Content-Type": "application/json"
51
+ }
52
+ });
53
+ }
54
+ catch (e) {
55
+ console.error(e);
56
+ }
57
+ }
58
+ }
59
+ exports.Notifier = Notifier;
@@ -0,0 +1,14 @@
1
+ import { BaseService } from "../BaseService";
2
+ import { IConfig } from "./ConfigService";
3
+ import { IDeploymentNotifier } from "./Notifier";
4
+ export declare class Watchdog extends BaseService {
5
+ config: Map<string, IConfig>;
6
+ lastStatuses: Map<string, boolean>;
7
+ notifier: IDeploymentNotifier | null;
8
+ constructor(notifier?: IDeploymentNotifier | null);
9
+ applicationReady(): void;
10
+ setup(): Promise<void>;
11
+ checkReadiness(config: IConfig): Promise<void>;
12
+ statusPassed(config: IConfig): void;
13
+ statusFailed(config: IConfig): void;
14
+ }