@whyour/qinglong 2.19.2-2 → 2.20.0-2
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/.env.example +1 -2
- package/README-en.md +4 -2
- package/README.md +4 -2
- package/back/protos/api.proto +17 -0
- package/docker/310.Dockerfile +23 -6
- package/docker/Dockerfile +22 -6
- package/docker/docker-entrypoint.sh +27 -14
- package/package.json +8 -9
- package/sample/notify.js +18 -2
- package/sample/notify.py +15 -0
- package/sample/ql_sample.js +28 -0
- package/shell/api.sh +8 -48
- package/shell/check.sh +5 -22
- package/shell/preload/client.js +6 -1
- package/shell/pub.sh +4 -4
- package/shell/share.sh +32 -55
- package/shell/start.sh +2 -3
- package/shell/task.sh +19 -10
- package/shell/update.sh +1 -0
- package/static/build/api/dependence.js +7 -1
- package/static/build/api/env.js +30 -4
- package/static/build/api/script.js +48 -8
- package/static/build/api/subscription.js +3 -3
- package/static/build/api/system.js +19 -26
- package/static/build/api/user.js +2 -1
- package/static/build/app.js +96 -18
- package/static/build/config/index.js +2 -2
- package/static/build/config/util.js +24 -1
- package/static/build/data/cron.js +4 -0
- package/static/build/data/env.js +3 -1
- package/static/build/data/notify.js +1 -0
- package/static/build/loaders/db.js +29 -35
- package/static/build/loaders/deps.js +22 -5
- package/static/build/loaders/express.js +19 -10
- package/static/build/loaders/initData.js +25 -1
- package/static/build/loaders/initTask.js +6 -0
- package/static/build/loaders/sock.js +10 -12
- package/static/build/protos/api.js +336 -1
- package/static/build/schedule/addCron.js +2 -2
- package/static/build/schedule/api.js +100 -1
- package/static/build/schedule/delCron.js +1 -1
- package/static/build/schedule/health.js +2 -3
- package/static/build/services/cron.js +54 -20
- package/static/build/services/dependence.js +6 -5
- package/static/build/services/env.js +9 -2
- package/static/build/services/notify.js +17 -5
- package/static/build/services/schedule.js +4 -4
- package/static/build/services/sshKey.js +24 -4
- package/static/build/services/subscription.js +11 -8
- package/static/build/services/system.js +15 -0
- package/static/build/services/user.js +83 -4
- package/static/build/shared/auth.js +40 -0
- package/static/build/shared/logStreamManager.js +104 -0
- package/static/build/shared/runCron.js +23 -0
- package/static/build/validation/schedule.js +39 -2
- package/static/dist/1147.856bb861.async.js +1 -0
- package/static/dist/1379.f91563a1.async.js +1 -0
- package/static/dist/{2208.3bc521b1.async.js → 2208.7bf7e296.async.js} +1 -1
- package/static/dist/3191.da7f3e07.async.js +1 -0
- package/static/dist/5691.931f59c5.async.js +1 -0
- package/static/dist/7571.4f6240b1.async.js +1 -0
- package/static/dist/{8826.3ab4ad84.async.js → 8826.5f289c4d.async.js} +1 -1
- package/static/dist/index.html +2 -2
- package/static/dist/preload_helper.0fb920eb.js +1 -0
- package/static/dist/{src__pages__crontab__detail.ee431270.async.js → src__pages__crontab__detail.b07f0c0a.async.js} +1 -1
- package/static/dist/src__pages__crontab__index.6b90d8c5.async.js +1 -0
- package/static/dist/{src__pages__crontab__logModal.57501983.async.js → src__pages__crontab__logModal.5e6a4bf2.async.js} +1 -1
- package/static/dist/src__pages__crontab__modal.2d3d4953.async.js +1 -0
- package/static/dist/src__pages__dependence__modal.86604072.async.js +1 -0
- package/static/dist/{src__pages__env__editNameModal.665393cd.async.js → src__pages__env__editNameModal.79b7cf83.async.js} +1 -1
- package/static/dist/src__pages__env__index.a0a2fece.async.js +1 -0
- package/static/dist/{src__pages__env__modal.168498f9.async.js → src__pages__env__modal.b84c1173.async.js} +1 -1
- package/static/dist/{src__pages__error__index.d9beeda3.async.js → src__pages__error__index.01fac00e.async.js} +1 -1
- package/static/dist/{src__pages__initialization__index.2403c031.async.js → src__pages__initialization__index.2e49cf43.async.js} +1 -1
- package/static/dist/src__pages__script__editModal.cbf4ec0e.async.js +1 -0
- package/static/dist/{src__pages__script__editNameModal.e36cd111.async.js → src__pages__script__editNameModal.05441c89.async.js} +1 -1
- package/static/dist/src__pages__script__index.d6e9cb23.async.js +1 -0
- package/static/dist/{src__pages__script__renameModal.f9756f26.async.js → src__pages__script__renameModal.3bb00014.async.js} +1 -1
- package/static/dist/src__pages__script__saveModal.8417503a.async.js +1 -0
- package/static/dist/{src__pages__script__setting.8c2727b4.async.js → src__pages__script__setting.5a2a2a2c.async.js} +1 -1
- package/static/dist/{src__pages__setting__appModal.5a39121e.async.js → src__pages__setting__appModal.7f763fa7.async.js} +1 -1
- package/static/dist/src__pages__setting__dependence.e64c4554.async.js +1 -0
- package/static/dist/src__pages__setting__index.3a220288.async.js +1 -0
- package/static/dist/src__pages__setting__notification.49003b2f.async.js +1 -0
- package/static/dist/src__pages__setting__other.0d931d6f.async.js +1 -0
- package/static/dist/src__pages__setting__security.a916e056.async.js +1 -0
- package/static/dist/{src__pages__setting__systemLog.fc5bdc78.async.js → src__pages__setting__systemLog.cbb0a3bb.async.js} +1 -1
- package/static/dist/src__pages__subscription__modal.ade477c1.async.js +1 -0
- package/static/dist/umi.e7cba995.js +1 -0
- package/version.yaml +46 -9
- package/docker/front.conf +0 -61
- package/docker/nginx.conf +0 -45
- package/static/dist/2995.2eb218b3.async.js +0 -1
- package/static/dist/3191.cc1e31cd.async.js +0 -1
- package/static/dist/4046.7fbcfa02.async.js +0 -1
- package/static/dist/5713.8519f547.async.js +0 -1
- package/static/dist/8851.503b1e64.async.js +0 -1
- package/static/dist/preload_helper.9c086410.js +0 -1
- package/static/dist/src__pages__crontab__index.af4cb04a.async.js +0 -1
- package/static/dist/src__pages__crontab__modal.21258e08.async.js +0 -1
- package/static/dist/src__pages__dependence__modal.6639424a.async.js +0 -1
- package/static/dist/src__pages__env__index.70340ba7.async.js +0 -1
- package/static/dist/src__pages__script__editModal.f1741417.async.js +0 -1
- package/static/dist/src__pages__script__index.82b42e11.async.js +0 -1
- package/static/dist/src__pages__script__saveModal.e885e133.async.js +0 -1
- package/static/dist/src__pages__setting__dependence.a46e873d.async.js +0 -1
- package/static/dist/src__pages__setting__index.9be4775c.async.js +0 -1
- package/static/dist/src__pages__setting__notification.299f6b96.async.js +0 -1
- package/static/dist/src__pages__setting__other.60924a56.async.js +0 -1
- package/static/dist/src__pages__setting__security.e7371daa.async.js +0 -1
- package/static/dist/src__pages__subscription__modal.a7fd6a3c.async.js +0 -1
- package/static/dist/umi.5b8ae363.js +0 -1
|
@@ -86,23 +86,24 @@ let DependenceService = class DependenceService {
|
|
|
86
86
|
await dependence_1.DependenceModel.destroy({ where: { id: ids } });
|
|
87
87
|
}
|
|
88
88
|
async dependencies({ searchValue, type, status, }, sort = [], query = {}) {
|
|
89
|
-
let condition =
|
|
89
|
+
let condition = query;
|
|
90
|
+
if (dependence_1.DependenceTypes[type]) {
|
|
91
|
+
condition.type = dependence_1.DependenceTypes[type];
|
|
92
|
+
}
|
|
90
93
|
if (status) {
|
|
91
94
|
condition.status = status.split(',').map(Number);
|
|
92
95
|
}
|
|
93
96
|
if (searchValue) {
|
|
94
97
|
const encodeText = encodeURI(searchValue);
|
|
95
|
-
|
|
98
|
+
condition.name = {
|
|
96
99
|
[sequelize_1.Op.or]: [
|
|
97
100
|
{ [sequelize_1.Op.like]: `%${searchValue}%` },
|
|
98
101
|
{ [sequelize_1.Op.like]: `%${encodeText}%` },
|
|
99
102
|
],
|
|
100
103
|
};
|
|
101
|
-
condition = Object.assign(Object.assign({}, condition), { name: reg });
|
|
102
104
|
}
|
|
103
105
|
try {
|
|
104
|
-
|
|
105
|
-
return result;
|
|
106
|
+
return await this.find(condition, sort);
|
|
106
107
|
}
|
|
107
108
|
catch (error) {
|
|
108
109
|
throw error;
|
|
@@ -15,12 +15,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
15
15
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const groupBy_1 = __importDefault(require("lodash/groupBy"));
|
|
19
|
+
const sequelize_1 = require("sequelize");
|
|
18
20
|
const typedi_1 = require("typedi");
|
|
19
21
|
const winston_1 = __importDefault(require("winston"));
|
|
20
22
|
const config_1 = __importDefault(require("../config"));
|
|
21
23
|
const env_1 = require("../data/env");
|
|
22
|
-
const groupBy_1 = __importDefault(require("lodash/groupBy"));
|
|
23
|
-
const sequelize_1 = require("sequelize");
|
|
24
24
|
const utils_1 = require("../shared/utils");
|
|
25
25
|
let EnvService = class EnvService {
|
|
26
26
|
constructor(logger) {
|
|
@@ -128,6 +128,7 @@ let EnvService = class EnvService {
|
|
|
128
128
|
}
|
|
129
129
|
try {
|
|
130
130
|
const result = await this.find(condition, [
|
|
131
|
+
['isPinned', 'DESC'],
|
|
131
132
|
['position', 'DESC'],
|
|
132
133
|
['createdAt', 'ASC'],
|
|
133
134
|
]);
|
|
@@ -163,6 +164,12 @@ let EnvService = class EnvService {
|
|
|
163
164
|
await env_1.EnvModel.update({ name }, { where: { id: ids } });
|
|
164
165
|
await this.set_envs();
|
|
165
166
|
}
|
|
167
|
+
async pin(ids) {
|
|
168
|
+
await env_1.EnvModel.update({ isPinned: 1 }, { where: { id: ids } });
|
|
169
|
+
}
|
|
170
|
+
async unPin(ids) {
|
|
171
|
+
await env_1.EnvModel.update({ isPinned: 0 }, { where: { id: ids } });
|
|
172
|
+
}
|
|
166
173
|
async set_envs() {
|
|
167
174
|
const envs = await this.envs('', {
|
|
168
175
|
name: { [sequelize_1.Op.not]: null },
|
|
@@ -454,15 +454,27 @@ let NotificationService = class NotificationService {
|
|
|
454
454
|
}
|
|
455
455
|
}
|
|
456
456
|
async lark() {
|
|
457
|
-
let { larkKey } = this.params;
|
|
457
|
+
let { larkKey, larkSecret } = this.params;
|
|
458
458
|
if (!larkKey.startsWith('http')) {
|
|
459
459
|
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
|
|
460
460
|
}
|
|
461
|
+
const body = {
|
|
462
|
+
msg_type: 'text',
|
|
463
|
+
content: { text: `${this.title}\n\n${this.content}` },
|
|
464
|
+
};
|
|
465
|
+
// Add signature if secret is provided
|
|
466
|
+
// Note: Feishu's signature algorithm uses timestamp+"\n"+secret as the HMAC key
|
|
467
|
+
// and signs an empty message, which differs from typical HMAC usage
|
|
468
|
+
if (larkSecret) {
|
|
469
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
470
|
+
const stringToSign = `${timestamp}\n${larkSecret}`;
|
|
471
|
+
const hmac = crypto_1.default.createHmac('sha256', stringToSign);
|
|
472
|
+
const sign = hmac.digest('base64');
|
|
473
|
+
body.timestamp = timestamp;
|
|
474
|
+
body.sign = sign;
|
|
475
|
+
}
|
|
461
476
|
try {
|
|
462
|
-
const res = await http_1.httpClient.post(larkKey, Object.assign(Object.assign({}, this.gotOption), { json: {
|
|
463
|
-
msg_type: 'text',
|
|
464
|
-
content: { text: `${this.title}\n\n${this.content}` },
|
|
465
|
-
}, headers: { 'Content-Type': 'application/json' } }));
|
|
477
|
+
const res = await http_1.httpClient.post(larkKey, Object.assign(Object.assign({}, this.gotOption), { json: body, headers: { 'Content-Type': 'application/json' } }));
|
|
466
478
|
if (res.StatusCode === 0 || res.code === 0) {
|
|
467
479
|
return true;
|
|
468
480
|
}
|
|
@@ -87,7 +87,7 @@ let ScheduleService = class ScheduleService {
|
|
|
87
87
|
}
|
|
88
88
|
async createCronTask({ id = 0, command, name, schedule = '', runOrigin }, callbacks, runImmediately = false) {
|
|
89
89
|
const _id = this.formatId(id);
|
|
90
|
-
this.logger.info('[panel][创建cron任务]
|
|
90
|
+
this.logger.info('[panel][创建cron任务] 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s', _id, schedule, name, command);
|
|
91
91
|
this.scheduleStacks.set(_id, node_schedule_1.default.scheduleJob(_id, schedule, async () => {
|
|
92
92
|
this.runTask(command, callbacks, {
|
|
93
93
|
name,
|
|
@@ -110,7 +110,7 @@ let ScheduleService = class ScheduleService {
|
|
|
110
110
|
async cancelCronTask({ id = 0, name }) {
|
|
111
111
|
var _a;
|
|
112
112
|
const _id = this.formatId(id);
|
|
113
|
-
this.logger.info('[panel][取消定时任务]
|
|
113
|
+
this.logger.info('[panel][取消定时任务] 任务名: %s', name);
|
|
114
114
|
if (this.scheduleStacks.has(_id)) {
|
|
115
115
|
(_a = this.scheduleStacks.get(_id)) === null || _a === void 0 ? void 0 : _a.cancel();
|
|
116
116
|
this.scheduleStacks.delete(_id);
|
|
@@ -118,7 +118,7 @@ let ScheduleService = class ScheduleService {
|
|
|
118
118
|
}
|
|
119
119
|
async createIntervalTask({ id = 0, command, name = '', runOrigin }, schedule, runImmediately = true, callbacks) {
|
|
120
120
|
const _id = this.formatId(id);
|
|
121
|
-
this.logger.info('[panel][创建interval任务]
|
|
121
|
+
this.logger.info('[panel][创建interval任务] 任务ID: %s, 任务名: %s, 执行命令: %s', _id, name, command);
|
|
122
122
|
const task = new toad_scheduler_1.Task(name, () => {
|
|
123
123
|
this.runTask(command, callbacks, {
|
|
124
124
|
name,
|
|
@@ -142,7 +142,7 @@ let ScheduleService = class ScheduleService {
|
|
|
142
142
|
}
|
|
143
143
|
async cancelIntervalTask({ id = 0, name }) {
|
|
144
144
|
const _id = this.formatId(id);
|
|
145
|
-
this.logger.info('[panel][取消interval任务]
|
|
145
|
+
this.logger.info('[panel][取消interval任务] 任务ID: %s, 任务名: %s', _id, name);
|
|
146
146
|
this.intervalSchedule.removeById(_id);
|
|
147
147
|
}
|
|
148
148
|
formatId(id) {
|
|
@@ -40,16 +40,15 @@ let SshKeyService = class SshKeyService {
|
|
|
40
40
|
config = await promises_1.default.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
43
|
-
await (0, utils_1.writeFileWithLock)(this.sshConfigFilePath, '');
|
|
43
|
+
await (0, utils_1.writeFileWithLock)(this.sshConfigFilePath, '', { mode: '600' });
|
|
44
44
|
}
|
|
45
45
|
if (!config.includes(this.sshConfigHeader)) {
|
|
46
|
-
await (0, utils_1.writeFileWithLock)(this.sshConfigFilePath, `${this.sshConfigHeader}\n\n${config}
|
|
46
|
+
await (0, utils_1.writeFileWithLock)(this.sshConfigFilePath, `${this.sshConfigHeader}\n\n${config}`, { mode: '600' });
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
async generatePrivateKeyFile(alias, key) {
|
|
50
50
|
try {
|
|
51
51
|
await (0, utils_1.writeFileWithLock)(path_1.default.join(this.sshPath, alias), `${key}${os_1.default.EOL}`, {
|
|
52
|
-
encoding: 'utf8',
|
|
53
52
|
mode: '400',
|
|
54
53
|
});
|
|
55
54
|
}
|
|
@@ -74,7 +73,10 @@ let SshKeyService = class SshKeyService {
|
|
|
74
73
|
? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
|
|
75
74
|
: '';
|
|
76
75
|
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path_1.default.join(this.sshPath, alias)}\n StrictHostKeyChecking no\n${proxyStr}`;
|
|
77
|
-
await (0, utils_1.writeFileWithLock)(`${path_1.default.join(this.sshPath, `${alias}.config`)}`, config
|
|
76
|
+
await (0, utils_1.writeFileWithLock)(`${path_1.default.join(this.sshPath, `${alias}.config`)}`, config, {
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
mode: '600',
|
|
79
|
+
});
|
|
78
80
|
}
|
|
79
81
|
async removeSshConfig(alias) {
|
|
80
82
|
try {
|
|
@@ -105,6 +107,24 @@ let SshKeyService = class SshKeyService {
|
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
}
|
|
110
|
+
async addGlobalSSHKey(key, alias) {
|
|
111
|
+
await this.generatePrivateKeyFile(`~global_${alias}`, key);
|
|
112
|
+
// Create a global SSH config entry that matches all hosts
|
|
113
|
+
// This allows the key to be used for any Git repository
|
|
114
|
+
await this.generateGlobalSshConfig(`~global_${alias}`);
|
|
115
|
+
}
|
|
116
|
+
async removeGlobalSSHKey(alias) {
|
|
117
|
+
await this.removePrivateKeyFile(`~global_${alias}`);
|
|
118
|
+
await this.removeSshConfig(`~global_${alias}`);
|
|
119
|
+
}
|
|
120
|
+
async generateGlobalSshConfig(alias) {
|
|
121
|
+
// Create a config that matches all hosts, making this key globally available
|
|
122
|
+
const config = `Host *\n IdentityFile ${path_1.default.join(this.sshPath, alias)}\n StrictHostKeyChecking no\n`;
|
|
123
|
+
await (0, utils_1.writeFileWithLock)(`${path_1.default.join(this.sshPath, `${alias}.config`)}`, config, {
|
|
124
|
+
encoding: 'utf8',
|
|
125
|
+
mode: '600',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
108
128
|
};
|
|
109
129
|
SshKeyService = __decorate([
|
|
110
130
|
(0, typedi_1.Service)(),
|
|
@@ -53,6 +53,7 @@ const const_1 = require("../config/const");
|
|
|
53
53
|
const subscription_2 = require("../config/subscription");
|
|
54
54
|
const cron_1 = require("../data/cron");
|
|
55
55
|
const cron_2 = __importDefault(require("./cron"));
|
|
56
|
+
const logStreamManager_1 = require("../shared/logStreamManager");
|
|
56
57
|
let SubscriptionService = class SubscriptionService {
|
|
57
58
|
constructor(logger, scheduleService, sockService, sshKeyService, crontabService) {
|
|
58
59
|
this.logger = logger;
|
|
@@ -129,7 +130,7 @@ let SubscriptionService = class SubscriptionService {
|
|
|
129
130
|
let beforeStr = '';
|
|
130
131
|
try {
|
|
131
132
|
if (doc.sub_before) {
|
|
132
|
-
await
|
|
133
|
+
await logStreamManager_1.logStreamManager.write(absolutePath, `\n## 执行before命令...\n\n`);
|
|
133
134
|
beforeStr = await (0, util_1.promiseExec)(doc.sub_before);
|
|
134
135
|
}
|
|
135
136
|
}
|
|
@@ -138,7 +139,7 @@ let SubscriptionService = class SubscriptionService {
|
|
|
138
139
|
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
|
139
140
|
}
|
|
140
141
|
if (beforeStr) {
|
|
141
|
-
await
|
|
142
|
+
await logStreamManager_1.logStreamManager.write(absolutePath, `${beforeStr}\n`);
|
|
142
143
|
}
|
|
143
144
|
},
|
|
144
145
|
onStart: async (cp, startTime) => {
|
|
@@ -153,7 +154,7 @@ let SubscriptionService = class SubscriptionService {
|
|
|
153
154
|
let afterStr = '';
|
|
154
155
|
try {
|
|
155
156
|
if (sub.sub_after) {
|
|
156
|
-
await
|
|
157
|
+
await logStreamManager_1.logStreamManager.write(absolutePath, `\n\n## 执行after命令...\n\n`);
|
|
157
158
|
afterStr = await (0, util_1.promiseExec)(sub.sub_after);
|
|
158
159
|
}
|
|
159
160
|
}
|
|
@@ -162,9 +163,11 @@ let SubscriptionService = class SubscriptionService {
|
|
|
162
163
|
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
|
|
163
164
|
}
|
|
164
165
|
if (afterStr) {
|
|
165
|
-
await
|
|
166
|
+
await logStreamManager_1.logStreamManager.write(absolutePath, `${afterStr}\n`);
|
|
166
167
|
}
|
|
167
|
-
await
|
|
168
|
+
await logStreamManager_1.logStreamManager.write(absolutePath, `\n## 执行结束... ${endTime.format('YYYY-MM-DD HH:mm:ss')} 耗时 ${diff} 秒${const_1.LOG_END_SYMBOL}`);
|
|
169
|
+
// Close the stream after task completion
|
|
170
|
+
await logStreamManager_1.logStreamManager.closeStream(absolutePath);
|
|
168
171
|
await subscription_1.SubscriptionModel.update({ status: subscription_1.SubscriptionStatus.idle, pid: undefined }, { where: { id: sub.id } });
|
|
169
172
|
this.sockService.sendMessage({
|
|
170
173
|
type: 'runSubscriptionEnd',
|
|
@@ -175,12 +178,12 @@ let SubscriptionService = class SubscriptionService {
|
|
|
175
178
|
onError: async (message) => {
|
|
176
179
|
const sub = await this.getDb({ id: doc.id });
|
|
177
180
|
const absolutePath = await (0, util_1.handleLogPath)(sub.log_path);
|
|
178
|
-
await
|
|
181
|
+
await logStreamManager_1.logStreamManager.write(absolutePath, `\n${message}`);
|
|
179
182
|
},
|
|
180
183
|
onLog: async (message) => {
|
|
181
184
|
const sub = await this.getDb({ id: doc.id });
|
|
182
185
|
const absolutePath = await (0, util_1.handleLogPath)(sub.log_path);
|
|
183
|
-
await
|
|
186
|
+
await logStreamManager_1.logStreamManager.write(absolutePath, `\n${message}`);
|
|
184
187
|
},
|
|
185
188
|
};
|
|
186
189
|
}
|
|
@@ -317,7 +320,7 @@ let SubscriptionService = class SubscriptionService {
|
|
|
317
320
|
return (await Promise.all(files.map(async (x) => ({
|
|
318
321
|
filename: x,
|
|
319
322
|
directory: relativeDir.replace(config_1.default.logPath, ''),
|
|
320
|
-
time: (await promises_1.default.lstat(`${dir}/${x}`)).
|
|
323
|
+
time: (await promises_1.default.lstat(`${dir}/${x}`)).birthtimeMs,
|
|
321
324
|
})))).sort((a, b) => b.time - a.time);
|
|
322
325
|
}
|
|
323
326
|
}
|
|
@@ -414,6 +414,21 @@ let SystemService = class SystemService {
|
|
|
414
414
|
return { code: 400, message: '设置时区失败' };
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
|
+
async updateGlobalSshKey(info) {
|
|
418
|
+
const oDoc = await this.getSystemConfig();
|
|
419
|
+
const result = await this.updateAuthDb(Object.assign(Object.assign({}, oDoc), { info: Object.assign(Object.assign({}, oDoc.info), info) }));
|
|
420
|
+
// Apply the global SSH key
|
|
421
|
+
const SshKeyService = require('./sshKey').default;
|
|
422
|
+
const Container = require('typedi').Container;
|
|
423
|
+
const sshKeyService = Container.get(SshKeyService);
|
|
424
|
+
if (info.globalSshKey) {
|
|
425
|
+
await sshKeyService.addGlobalSSHKey(info.globalSshKey, 'global');
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
await sshKeyService.removeGlobalSSHKey('global');
|
|
429
|
+
}
|
|
430
|
+
return { code: 200, data: result };
|
|
431
|
+
}
|
|
417
432
|
async cleanDependence(type) {
|
|
418
433
|
if (!type || !['node', 'python3'].includes(type)) {
|
|
419
434
|
return { code: 400, message: '参数错误' };
|
|
@@ -79,9 +79,17 @@ let UserService = class UserService {
|
|
|
79
79
|
expiresIn: config_1.default.jwt.expiresIn || expiration,
|
|
80
80
|
algorithm: 'HS384',
|
|
81
81
|
});
|
|
82
|
+
const tokenInfo = {
|
|
83
|
+
value: token,
|
|
84
|
+
timestamp,
|
|
85
|
+
ip,
|
|
86
|
+
address,
|
|
87
|
+
platform: req.platform,
|
|
88
|
+
};
|
|
89
|
+
const updatedTokens = this.addTokenToList(tokens, req.platform, tokenInfo);
|
|
82
90
|
await this.updateAuthInfo(content, {
|
|
83
91
|
token,
|
|
84
|
-
tokens:
|
|
92
|
+
tokens: updatedTokens,
|
|
85
93
|
lastlogon: timestamp,
|
|
86
94
|
retries: 0,
|
|
87
95
|
lastip: ip,
|
|
@@ -146,11 +154,23 @@ let UserService = class UserService {
|
|
|
146
154
|
}
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
|
-
async logout(platform) {
|
|
157
|
+
async logout(platform, tokenValue) {
|
|
158
|
+
if (!platform || !tokenValue) {
|
|
159
|
+
this.logger.warn('Invalid logout parameters - empty platform or token');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
150
162
|
const authInfo = await this.getAuthInfo();
|
|
163
|
+
// Verify the token exists before attempting to remove it
|
|
164
|
+
const tokenExists = this.findTokenInList(authInfo.tokens, platform, tokenValue);
|
|
165
|
+
if (!tokenExists && authInfo.token !== tokenValue) {
|
|
166
|
+
// Token not found, but don't throw error - user may have already logged out
|
|
167
|
+
this.logger.info(`Logout attempted for non-existent token on platform: ${platform}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const updatedTokens = this.removeTokenFromList(authInfo.tokens, platform, tokenValue);
|
|
151
171
|
await this.updateAuthInfo(authInfo, {
|
|
152
|
-
token: '',
|
|
153
|
-
tokens:
|
|
172
|
+
token: authInfo.token === tokenValue ? '' : authInfo.token,
|
|
173
|
+
tokens: updatedTokens,
|
|
154
174
|
});
|
|
155
175
|
}
|
|
156
176
|
async getLoginLog() {
|
|
@@ -299,6 +319,65 @@ let UserService = class UserService {
|
|
|
299
319
|
return { code: 400, message: '通知发送失败,请检查参数' };
|
|
300
320
|
}
|
|
301
321
|
}
|
|
322
|
+
normalizeTokens(tokens) {
|
|
323
|
+
const normalized = {};
|
|
324
|
+
for (const [platform, value] of Object.entries(tokens)) {
|
|
325
|
+
if (typeof value === 'string') {
|
|
326
|
+
// Legacy format: convert string token to TokenInfo array
|
|
327
|
+
if (value) {
|
|
328
|
+
normalized[platform] = [
|
|
329
|
+
{
|
|
330
|
+
value,
|
|
331
|
+
timestamp: Date.now(),
|
|
332
|
+
ip: '',
|
|
333
|
+
address: '',
|
|
334
|
+
platform,
|
|
335
|
+
},
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
normalized[platform] = [];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
// Already in new format
|
|
344
|
+
normalized[platform] = value || [];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return normalized;
|
|
348
|
+
}
|
|
349
|
+
addTokenToList(tokens, platform, tokenInfo, maxTokensPerPlatform = config_1.default.maxTokensPerPlatform) {
|
|
350
|
+
// Validate maxTokensPerPlatform parameter
|
|
351
|
+
if (!Number.isInteger(maxTokensPerPlatform) || maxTokensPerPlatform < 1) {
|
|
352
|
+
this.logger.warn(`Invalid maxTokensPerPlatform value: ${maxTokensPerPlatform}, using default`);
|
|
353
|
+
maxTokensPerPlatform = config_1.default.maxTokensPerPlatform;
|
|
354
|
+
}
|
|
355
|
+
const normalized = this.normalizeTokens(tokens);
|
|
356
|
+
if (!normalized[platform]) {
|
|
357
|
+
normalized[platform] = [];
|
|
358
|
+
}
|
|
359
|
+
// Add new token
|
|
360
|
+
normalized[platform].unshift(tokenInfo);
|
|
361
|
+
// Limit the number of active tokens per platform
|
|
362
|
+
if (normalized[platform].length > maxTokensPerPlatform) {
|
|
363
|
+
normalized[platform] = normalized[platform].slice(0, maxTokensPerPlatform);
|
|
364
|
+
}
|
|
365
|
+
return normalized;
|
|
366
|
+
}
|
|
367
|
+
removeTokenFromList(tokens, platform, tokenValue) {
|
|
368
|
+
const normalized = this.normalizeTokens(tokens);
|
|
369
|
+
if (normalized[platform]) {
|
|
370
|
+
normalized[platform] = normalized[platform].filter((t) => t.value !== tokenValue);
|
|
371
|
+
}
|
|
372
|
+
return normalized;
|
|
373
|
+
}
|
|
374
|
+
findTokenInList(tokens, platform, tokenValue) {
|
|
375
|
+
const normalized = this.normalizeTokens(tokens);
|
|
376
|
+
if (normalized[platform]) {
|
|
377
|
+
return normalized[platform].find((t) => t.value === tokenValue);
|
|
378
|
+
}
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
302
381
|
async resetAuthInfo(info) {
|
|
303
382
|
const { retries, twoFactorActivated, password, username } = info;
|
|
304
383
|
const authInfo = await this.getAuthInfo();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isValidToken = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Validates if a token exists in the authentication info.
|
|
6
|
+
* Supports both legacy string tokens and new TokenInfo array format.
|
|
7
|
+
*
|
|
8
|
+
* @param authInfo - The authentication information
|
|
9
|
+
* @param headerToken - The token to validate
|
|
10
|
+
* @param platform - The platform (desktop, mobile)
|
|
11
|
+
* @returns true if the token is valid, false otherwise
|
|
12
|
+
*/
|
|
13
|
+
function isValidToken(authInfo, headerToken, platform) {
|
|
14
|
+
if (!authInfo || !headerToken) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const { token = '', tokens = {} } = authInfo;
|
|
18
|
+
// Check legacy token field
|
|
19
|
+
if (headerToken === token) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
// Check platform-specific tokens (support both legacy string and new TokenInfo[] format)
|
|
23
|
+
const platformTokens = tokens[platform];
|
|
24
|
+
// Handle null/undefined platformTokens
|
|
25
|
+
if (platformTokens === null || platformTokens === undefined) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (typeof platformTokens === 'string') {
|
|
29
|
+
// Legacy format: single string token
|
|
30
|
+
return headerToken === platformTokens;
|
|
31
|
+
}
|
|
32
|
+
else if (Array.isArray(platformTokens)) {
|
|
33
|
+
// New format: array of TokenInfo objects
|
|
34
|
+
return platformTokens.some((t) => t && t.value === headerToken);
|
|
35
|
+
}
|
|
36
|
+
// Unexpected type - log warning and reject
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
exports.isValidToken = isValidToken;
|
|
40
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logStreamManager = exports.LogStreamManager = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const events_1 = require("events");
|
|
6
|
+
/**
|
|
7
|
+
* Manages write streams for log files to improve performance by avoiding repeated file opens
|
|
8
|
+
*/
|
|
9
|
+
class LogStreamManager extends events_1.EventEmitter {
|
|
10
|
+
constructor() {
|
|
11
|
+
super(...arguments);
|
|
12
|
+
this.streams = new Map();
|
|
13
|
+
this.pendingWrites = new Map();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Write data to a log file using a managed stream
|
|
17
|
+
* @param filePath - Absolute path to the log file
|
|
18
|
+
* @param data - Data to write to the log file
|
|
19
|
+
*/
|
|
20
|
+
async write(filePath, data) {
|
|
21
|
+
// Wait for any pending writes to this file to complete
|
|
22
|
+
const pending = this.pendingWrites.get(filePath);
|
|
23
|
+
if (pending) {
|
|
24
|
+
await pending;
|
|
25
|
+
}
|
|
26
|
+
// Create a new promise for this write operation
|
|
27
|
+
const writePromise = new Promise((resolve, reject) => {
|
|
28
|
+
let stream = this.streams.get(filePath);
|
|
29
|
+
if (!stream) {
|
|
30
|
+
// Create a new write stream if one doesn't exist
|
|
31
|
+
stream = (0, fs_1.createWriteStream)(filePath, { flags: 'a' });
|
|
32
|
+
this.streams.set(filePath, stream);
|
|
33
|
+
// Handle stream errors
|
|
34
|
+
stream.on('error', (error) => {
|
|
35
|
+
this.emit('error', { filePath, error });
|
|
36
|
+
// Remove the stream from the map on error
|
|
37
|
+
this.streams.delete(filePath);
|
|
38
|
+
reject(error);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// Write the data
|
|
42
|
+
const canContinue = stream.write(data, 'utf8', (error) => {
|
|
43
|
+
if (error) {
|
|
44
|
+
reject(error);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
resolve();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
// Handle backpressure
|
|
51
|
+
if (!canContinue) {
|
|
52
|
+
stream.once('drain', () => {
|
|
53
|
+
// Stream is ready for more data
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
this.pendingWrites.set(filePath, writePromise);
|
|
58
|
+
try {
|
|
59
|
+
await writePromise;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
this.pendingWrites.delete(filePath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Close the stream for a specific file path
|
|
67
|
+
* @param filePath - Absolute path to the log file
|
|
68
|
+
*/
|
|
69
|
+
async closeStream(filePath) {
|
|
70
|
+
// Wait for any pending writes to complete
|
|
71
|
+
const pending = this.pendingWrites.get(filePath);
|
|
72
|
+
if (pending) {
|
|
73
|
+
await pending.catch(() => {
|
|
74
|
+
// Ignore errors on pending writes during close
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const stream = this.streams.get(filePath);
|
|
78
|
+
if (stream) {
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
stream.end(() => {
|
|
81
|
+
this.streams.delete(filePath);
|
|
82
|
+
resolve();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Close all open streams
|
|
89
|
+
*/
|
|
90
|
+
async closeAll() {
|
|
91
|
+
const closePromises = Array.from(this.streams.keys()).map((filePath) => this.closeStream(filePath));
|
|
92
|
+
await Promise.all(closePromises);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get the number of open streams
|
|
96
|
+
*/
|
|
97
|
+
getOpenStreamCount() {
|
|
98
|
+
return this.streams.size;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.LogStreamManager = LogStreamManager;
|
|
102
|
+
// Export a singleton instance for shared use
|
|
103
|
+
exports.logStreamManager = new LogStreamManager();
|
|
104
|
+
//# sourceMappingURL=logStreamManager.js.map
|
|
@@ -7,9 +7,32 @@ exports.runCron = void 0;
|
|
|
7
7
|
const cross_spawn_1 = require("cross-spawn");
|
|
8
8
|
const pLimit_1 = __importDefault(require("./pLimit"));
|
|
9
9
|
const logger_1 = __importDefault(require("../loaders/logger"));
|
|
10
|
+
const cron_1 = require("../data/cron");
|
|
11
|
+
const util_1 = require("../config/util");
|
|
10
12
|
function runCron(cmd, cron) {
|
|
11
13
|
return pLimit_1.default.runWithCronLimit(cron, () => {
|
|
12
14
|
return new Promise(async (resolve) => {
|
|
15
|
+
// Check if the cron is already running and stop it (only if multiple instances are not allowed)
|
|
16
|
+
try {
|
|
17
|
+
const existingCron = await cron_1.CrontabModel.findOne({
|
|
18
|
+
where: { id: Number(cron.id) },
|
|
19
|
+
});
|
|
20
|
+
// Default to single instance mode (0) for backward compatibility
|
|
21
|
+
const allowSingleInstances = (existingCron === null || existingCron === void 0 ? void 0 : existingCron.allow_multiple_instances) === 0;
|
|
22
|
+
if (allowSingleInstances &&
|
|
23
|
+
existingCron &&
|
|
24
|
+
existingCron.pid &&
|
|
25
|
+
(existingCron.status === cron_1.CrontabStatus.running ||
|
|
26
|
+
existingCron.status === cron_1.CrontabStatus.queued)) {
|
|
27
|
+
logger_1.default.info(`[schedule][停止已运行任务] 任务ID: ${cron.id}, PID: ${existingCron.pid}`);
|
|
28
|
+
await (0, util_1.killTask)(existingCron.pid);
|
|
29
|
+
// Update the status to idle after killing
|
|
30
|
+
await cron_1.CrontabModel.update({ status: cron_1.CrontabStatus.idle, pid: undefined }, { where: { id: Number(cron.id) } });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
logger_1.default.error(`[schedule][检查已运行任务失败] 任务ID: ${cron.id}, 错误: ${error}`);
|
|
35
|
+
}
|
|
13
36
|
logger_1.default.info(`[schedule][开始执行任务] 参数 ${JSON.stringify(Object.assign(Object.assign({}, cron), { command: cmd }))}`);
|
|
14
37
|
const cp = (0, cross_spawn_1.spawn)(cmd, { shell: '/bin/bash' });
|
|
15
38
|
cp.stderr.on('data', (data) => {
|
|
@@ -5,15 +5,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.commonCronSchema = exports.scheduleSchema = void 0;
|
|
7
7
|
const celebrate_1 = require("celebrate");
|
|
8
|
-
const cron_parser_1 =
|
|
8
|
+
const cron_parser_1 = require("cron-parser");
|
|
9
9
|
const schedule_1 = require("../interface/schedule");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const config_1 = __importDefault(require("../config"));
|
|
10
12
|
const validateSchedule = (value, helpers) => {
|
|
11
13
|
if (value.startsWith(schedule_1.ScheduleType.ONCE) ||
|
|
12
14
|
value.startsWith(schedule_1.ScheduleType.BOOT)) {
|
|
13
15
|
return value;
|
|
14
16
|
}
|
|
15
17
|
try {
|
|
16
|
-
if (cron_parser_1.
|
|
18
|
+
if (cron_parser_1.CronExpressionParser.parse(value).hasNext()) {
|
|
17
19
|
return value;
|
|
18
20
|
}
|
|
19
21
|
}
|
|
@@ -38,5 +40,40 @@ exports.commonCronSchema = {
|
|
|
38
40
|
extra_schedules: celebrate_1.Joi.array().optional().allow(null),
|
|
39
41
|
task_before: celebrate_1.Joi.string().optional().allow('').allow(null),
|
|
40
42
|
task_after: celebrate_1.Joi.string().optional().allow('').allow(null),
|
|
43
|
+
log_name: celebrate_1.Joi.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.allow('')
|
|
46
|
+
.allow(null)
|
|
47
|
+
.custom((value, helpers) => {
|
|
48
|
+
if (!value)
|
|
49
|
+
return value;
|
|
50
|
+
// Check if it's an absolute path
|
|
51
|
+
if (value.startsWith('/')) {
|
|
52
|
+
// Allow /dev/null as special case
|
|
53
|
+
if (value === '/dev/null') {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
// For other absolute paths, ensure they are within the safe log directory
|
|
57
|
+
const normalizedValue = path_1.default.normalize(value);
|
|
58
|
+
const normalizedLogPath = path_1.default.normalize(config_1.default.logPath);
|
|
59
|
+
if (!normalizedValue.startsWith(normalizedLogPath)) {
|
|
60
|
+
return helpers.error('string.unsafePath');
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if (!/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w.-]+\/)*[\w.-]+\/?$/.test(value)) {
|
|
65
|
+
return helpers.error('string.pattern.base');
|
|
66
|
+
}
|
|
67
|
+
if (value.length > 100) {
|
|
68
|
+
return helpers.error('string.max');
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
})
|
|
72
|
+
.messages({
|
|
73
|
+
'string.pattern.base': '日志名称只能包含字母、数字、下划线和连字符',
|
|
74
|
+
'string.max': '日志名称不能超过100个字符',
|
|
75
|
+
'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null',
|
|
76
|
+
}),
|
|
77
|
+
allow_multiple_instances: celebrate_1.Joi.number().optional().valid(0, 1).allow(null),
|
|
41
78
|
};
|
|
42
79
|
//# sourceMappingURL=schedule.js.map
|