@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.
Files changed (112) hide show
  1. package/.env.example +1 -2
  2. package/README-en.md +4 -2
  3. package/README.md +4 -2
  4. package/back/protos/api.proto +17 -0
  5. package/docker/310.Dockerfile +23 -6
  6. package/docker/Dockerfile +22 -6
  7. package/docker/docker-entrypoint.sh +27 -14
  8. package/package.json +8 -9
  9. package/sample/notify.js +18 -2
  10. package/sample/notify.py +15 -0
  11. package/sample/ql_sample.js +28 -0
  12. package/shell/api.sh +8 -48
  13. package/shell/check.sh +5 -22
  14. package/shell/preload/client.js +6 -1
  15. package/shell/pub.sh +4 -4
  16. package/shell/share.sh +32 -55
  17. package/shell/start.sh +2 -3
  18. package/shell/task.sh +19 -10
  19. package/shell/update.sh +1 -0
  20. package/static/build/api/dependence.js +7 -1
  21. package/static/build/api/env.js +30 -4
  22. package/static/build/api/script.js +48 -8
  23. package/static/build/api/subscription.js +3 -3
  24. package/static/build/api/system.js +19 -26
  25. package/static/build/api/user.js +2 -1
  26. package/static/build/app.js +96 -18
  27. package/static/build/config/index.js +2 -2
  28. package/static/build/config/util.js +24 -1
  29. package/static/build/data/cron.js +4 -0
  30. package/static/build/data/env.js +3 -1
  31. package/static/build/data/notify.js +1 -0
  32. package/static/build/loaders/db.js +29 -35
  33. package/static/build/loaders/deps.js +22 -5
  34. package/static/build/loaders/express.js +19 -10
  35. package/static/build/loaders/initData.js +25 -1
  36. package/static/build/loaders/initTask.js +6 -0
  37. package/static/build/loaders/sock.js +10 -12
  38. package/static/build/protos/api.js +336 -1
  39. package/static/build/schedule/addCron.js +2 -2
  40. package/static/build/schedule/api.js +100 -1
  41. package/static/build/schedule/delCron.js +1 -1
  42. package/static/build/schedule/health.js +2 -3
  43. package/static/build/services/cron.js +54 -20
  44. package/static/build/services/dependence.js +6 -5
  45. package/static/build/services/env.js +9 -2
  46. package/static/build/services/notify.js +17 -5
  47. package/static/build/services/schedule.js +4 -4
  48. package/static/build/services/sshKey.js +24 -4
  49. package/static/build/services/subscription.js +11 -8
  50. package/static/build/services/system.js +15 -0
  51. package/static/build/services/user.js +83 -4
  52. package/static/build/shared/auth.js +40 -0
  53. package/static/build/shared/logStreamManager.js +104 -0
  54. package/static/build/shared/runCron.js +23 -0
  55. package/static/build/validation/schedule.js +39 -2
  56. package/static/dist/1147.856bb861.async.js +1 -0
  57. package/static/dist/1379.f91563a1.async.js +1 -0
  58. package/static/dist/{2208.3bc521b1.async.js → 2208.7bf7e296.async.js} +1 -1
  59. package/static/dist/3191.da7f3e07.async.js +1 -0
  60. package/static/dist/5691.931f59c5.async.js +1 -0
  61. package/static/dist/7571.4f6240b1.async.js +1 -0
  62. package/static/dist/{8826.3ab4ad84.async.js → 8826.5f289c4d.async.js} +1 -1
  63. package/static/dist/index.html +2 -2
  64. package/static/dist/preload_helper.0fb920eb.js +1 -0
  65. package/static/dist/{src__pages__crontab__detail.ee431270.async.js → src__pages__crontab__detail.b07f0c0a.async.js} +1 -1
  66. package/static/dist/src__pages__crontab__index.6b90d8c5.async.js +1 -0
  67. package/static/dist/{src__pages__crontab__logModal.57501983.async.js → src__pages__crontab__logModal.5e6a4bf2.async.js} +1 -1
  68. package/static/dist/src__pages__crontab__modal.2d3d4953.async.js +1 -0
  69. package/static/dist/src__pages__dependence__modal.86604072.async.js +1 -0
  70. package/static/dist/{src__pages__env__editNameModal.665393cd.async.js → src__pages__env__editNameModal.79b7cf83.async.js} +1 -1
  71. package/static/dist/src__pages__env__index.a0a2fece.async.js +1 -0
  72. package/static/dist/{src__pages__env__modal.168498f9.async.js → src__pages__env__modal.b84c1173.async.js} +1 -1
  73. package/static/dist/{src__pages__error__index.d9beeda3.async.js → src__pages__error__index.01fac00e.async.js} +1 -1
  74. package/static/dist/{src__pages__initialization__index.2403c031.async.js → src__pages__initialization__index.2e49cf43.async.js} +1 -1
  75. package/static/dist/src__pages__script__editModal.cbf4ec0e.async.js +1 -0
  76. package/static/dist/{src__pages__script__editNameModal.e36cd111.async.js → src__pages__script__editNameModal.05441c89.async.js} +1 -1
  77. package/static/dist/src__pages__script__index.d6e9cb23.async.js +1 -0
  78. package/static/dist/{src__pages__script__renameModal.f9756f26.async.js → src__pages__script__renameModal.3bb00014.async.js} +1 -1
  79. package/static/dist/src__pages__script__saveModal.8417503a.async.js +1 -0
  80. package/static/dist/{src__pages__script__setting.8c2727b4.async.js → src__pages__script__setting.5a2a2a2c.async.js} +1 -1
  81. package/static/dist/{src__pages__setting__appModal.5a39121e.async.js → src__pages__setting__appModal.7f763fa7.async.js} +1 -1
  82. package/static/dist/src__pages__setting__dependence.e64c4554.async.js +1 -0
  83. package/static/dist/src__pages__setting__index.3a220288.async.js +1 -0
  84. package/static/dist/src__pages__setting__notification.49003b2f.async.js +1 -0
  85. package/static/dist/src__pages__setting__other.0d931d6f.async.js +1 -0
  86. package/static/dist/src__pages__setting__security.a916e056.async.js +1 -0
  87. package/static/dist/{src__pages__setting__systemLog.fc5bdc78.async.js → src__pages__setting__systemLog.cbb0a3bb.async.js} +1 -1
  88. package/static/dist/src__pages__subscription__modal.ade477c1.async.js +1 -0
  89. package/static/dist/umi.e7cba995.js +1 -0
  90. package/version.yaml +46 -9
  91. package/docker/front.conf +0 -61
  92. package/docker/nginx.conf +0 -45
  93. package/static/dist/2995.2eb218b3.async.js +0 -1
  94. package/static/dist/3191.cc1e31cd.async.js +0 -1
  95. package/static/dist/4046.7fbcfa02.async.js +0 -1
  96. package/static/dist/5713.8519f547.async.js +0 -1
  97. package/static/dist/8851.503b1e64.async.js +0 -1
  98. package/static/dist/preload_helper.9c086410.js +0 -1
  99. package/static/dist/src__pages__crontab__index.af4cb04a.async.js +0 -1
  100. package/static/dist/src__pages__crontab__modal.21258e08.async.js +0 -1
  101. package/static/dist/src__pages__dependence__modal.6639424a.async.js +0 -1
  102. package/static/dist/src__pages__env__index.70340ba7.async.js +0 -1
  103. package/static/dist/src__pages__script__editModal.f1741417.async.js +0 -1
  104. package/static/dist/src__pages__script__index.82b42e11.async.js +0 -1
  105. package/static/dist/src__pages__script__saveModal.e885e133.async.js +0 -1
  106. package/static/dist/src__pages__setting__dependence.a46e873d.async.js +0 -1
  107. package/static/dist/src__pages__setting__index.9be4775c.async.js +0 -1
  108. package/static/dist/src__pages__setting__notification.299f6b96.async.js +0 -1
  109. package/static/dist/src__pages__setting__other.60924a56.async.js +0 -1
  110. package/static/dist/src__pages__setting__security.e7371daa.async.js +0 -1
  111. package/static/dist/src__pages__subscription__modal.a7fd6a3c.async.js +0 -1
  112. 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 = Object.assign(Object.assign({}, query), { type: dependence_1.DependenceTypes[type] });
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
- const reg = {
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
- const result = await this.find(condition, sort);
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任务], 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s', _id, schedule, name, command);
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][取消定时任务], 任务名: %s', name);
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任务], 任务ID: %s, 任务名: %s, 执行命令: %s', _id, name, command);
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任务], 任务ID: %s, 任务名: %s', _id, name);
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 promises_1.default.appendFile(absolutePath, `\n## 执行before命令...\n\n`);
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 promises_1.default.appendFile(absolutePath, `${beforeStr}\n`);
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 promises_1.default.appendFile(absolutePath, `\n\n## 执行after命令...\n\n`);
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 promises_1.default.appendFile(absolutePath, `${afterStr}\n`);
166
+ await logStreamManager_1.logStreamManager.write(absolutePath, `${afterStr}\n`);
166
167
  }
167
- await promises_1.default.appendFile(absolutePath, `\n## 执行结束... ${endTime.format('YYYY-MM-DD HH:mm:ss')} 耗时 ${diff} 秒${const_1.LOG_END_SYMBOL}`);
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 promises_1.default.appendFile(absolutePath, `\n${message}`);
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 promises_1.default.appendFile(absolutePath, `\n${message}`);
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}`)).mtime.getTime(),
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: Object.assign(Object.assign({}, tokens), { [req.platform]: token }),
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: Object.assign(Object.assign({}, authInfo.tokens), { [platform]: '' }),
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 = __importDefault(require("cron-parser"));
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.default.parseExpression(value).hasNext()) {
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