izteamslots 1.5.0 → 1.5.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/CHANGELOG.md +14 -0
- package/backend/account_store.py +100 -92
- package/backend/jobs.py +0 -1
- package/backend/mail/boomlify.py +1 -1
- package/backend/rpc_server.py +5 -6
- package/backend/ui_facade.py +18 -7
- package/package.json +1 -1
- package/ui/src/menus/types.ts +0 -1
- package/ui/src/screens/MainScreen.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.5.2](https://github.com/izzzzzi/izTeamSlots/compare/v1.5.1...v1.5.2) (2026-03-06)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* security and thread-safety improvements from code review ([6356bfd](https://github.com/izzzzzi/izTeamSlots/commit/6356bfd9eded9379fe1424b20f954cec8e847c8b))
|
|
7
|
+
|
|
8
|
+
## [1.5.1](https://github.com/izzzzzi/izTeamSlots/compare/v1.5.0...v1.5.1) (2026-03-06)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* use self.password_prefix in boomlify generate() instead of hardcoded string ([de0098a](https://github.com/izzzzzi/izTeamSlots/commit/de0098ad620ee3010c359155b726f097b15fb13f))
|
|
14
|
+
|
|
1
15
|
# [1.5.0](https://github.com/izzzzzi/izTeamSlots/compare/v1.4.0...v1.5.0) (2026-03-06)
|
|
2
16
|
|
|
3
17
|
|
package/backend/account_store.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import shutil
|
|
5
|
+
import threading
|
|
5
6
|
import uuid
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from datetime import datetime, timezone
|
|
@@ -46,6 +47,7 @@ class AccountStore:
|
|
|
46
47
|
self.base_dir = base_dir
|
|
47
48
|
self.admin_dir = base_dir / "admin"
|
|
48
49
|
self.worker_dir = base_dir / "worker"
|
|
50
|
+
self._lock = threading.Lock()
|
|
49
51
|
self.admin_dir.mkdir(parents=True, exist_ok=True)
|
|
50
52
|
self.worker_dir.mkdir(parents=True, exist_ok=True)
|
|
51
53
|
|
|
@@ -113,49 +115,52 @@ class AccountStore:
|
|
|
113
115
|
)
|
|
114
116
|
|
|
115
117
|
def add_admin(self, email: str, password: str) -> AdminAccount:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
118
|
+
with self._lock:
|
|
119
|
+
index = self._read_index(self.admin_dir)
|
|
120
|
+
if email in index:
|
|
121
|
+
raise ValueError(f"Админ {email} уже существует")
|
|
122
|
+
account_id = uuid.uuid4().hex
|
|
123
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
124
|
+
index[email] = {"id": account_id, "created_at": now}
|
|
125
|
+
self._write_index(self.admin_dir, index)
|
|
126
|
+
account_dir = self.admin_dir / account_id
|
|
127
|
+
self._write_meta(account_dir, {"email": email, "password": password})
|
|
128
|
+
return AdminAccount(id=account_id, email=email, password=password, created_at=now)
|
|
126
129
|
|
|
127
130
|
def update_admin(self, account: AdminAccount) -> None:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
131
|
+
with self._lock:
|
|
132
|
+
index = self._read_index(self.admin_dir)
|
|
133
|
+
if account.email not in index:
|
|
134
|
+
raise ValueError(f"Админ {account.email} не найден")
|
|
135
|
+
info = index[account.email]
|
|
136
|
+
if account.last_login:
|
|
137
|
+
info["last_login"] = account.last_login
|
|
138
|
+
self._write_index(self.admin_dir, index)
|
|
139
|
+
account_dir = self.admin_dir / info["id"]
|
|
140
|
+
meta: dict[str, Any] = {
|
|
141
|
+
"email": account.email,
|
|
142
|
+
"password": account.password,
|
|
143
|
+
}
|
|
144
|
+
if account.access_token is not None:
|
|
145
|
+
meta["access_token"] = account.access_token
|
|
146
|
+
if account.workspace_id is not None:
|
|
147
|
+
meta["workspace_id"] = account.workspace_id
|
|
148
|
+
if account.account_id is not None:
|
|
149
|
+
meta["account_id"] = account.account_id
|
|
150
|
+
if account.workspaces is not None:
|
|
151
|
+
meta["workspaces"] = account.workspaces
|
|
152
|
+
self._write_meta(account_dir, meta)
|
|
149
153
|
|
|
150
154
|
def delete_admin(self, email: str) -> None:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
with self._lock:
|
|
156
|
+
index = self._read_index(self.admin_dir)
|
|
157
|
+
info = index.pop(email, None)
|
|
158
|
+
if not info:
|
|
159
|
+
return
|
|
160
|
+
self._write_index(self.admin_dir, index)
|
|
161
|
+
account_dir = self.admin_dir / info["id"]
|
|
162
|
+
if account_dir.exists():
|
|
163
|
+
shutil.rmtree(account_dir)
|
|
159
164
|
|
|
160
165
|
def get_admin_profile_dir(self, account: AdminAccount) -> Path:
|
|
161
166
|
profile_dir = self.admin_dir / account.id / "browser_profile"
|
|
@@ -203,63 +208,66 @@ class AccountStore:
|
|
|
203
208
|
)
|
|
204
209
|
|
|
205
210
|
def add_worker(self, email: str, password: str, admin_email: str) -> WorkerAccount:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
211
|
+
with self._lock:
|
|
212
|
+
index = self._read_index(self.worker_dir)
|
|
213
|
+
if email in index:
|
|
214
|
+
raise ValueError(f"Worker {email} уже существует")
|
|
215
|
+
worker_id = uuid.uuid4().hex
|
|
216
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
217
|
+
index[email] = {
|
|
218
|
+
"id": worker_id,
|
|
219
|
+
"status": "created",
|
|
220
|
+
"admin_email": admin_email,
|
|
221
|
+
"created_at": now,
|
|
222
|
+
}
|
|
223
|
+
self._write_index(self.worker_dir, index)
|
|
224
|
+
account_dir = self.worker_dir / worker_id
|
|
225
|
+
self._write_meta(account_dir, {
|
|
226
|
+
"email": email,
|
|
227
|
+
"password": password,
|
|
228
|
+
"status": "created",
|
|
229
|
+
})
|
|
230
|
+
return WorkerAccount(
|
|
231
|
+
id=worker_id, email=email, password=password,
|
|
232
|
+
admin_email=admin_email, created_at=now,
|
|
233
|
+
)
|
|
228
234
|
|
|
229
235
|
def update_worker(self, account: WorkerAccount) -> None:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
236
|
+
with self._lock:
|
|
237
|
+
index = self._read_index(self.worker_dir)
|
|
238
|
+
if account.email not in index:
|
|
239
|
+
raise ValueError(f"Worker {account.email} не найден")
|
|
240
|
+
info = index[account.email]
|
|
241
|
+
info["status"] = account.status
|
|
242
|
+
if account.admin_email is not None:
|
|
243
|
+
info["admin_email"] = account.admin_email
|
|
244
|
+
else:
|
|
245
|
+
info.pop("admin_email", None)
|
|
246
|
+
self._write_index(self.worker_dir, index)
|
|
247
|
+
account_dir = self.worker_dir / info["id"]
|
|
248
|
+
meta: dict[str, Any] = {
|
|
249
|
+
"email": account.email,
|
|
250
|
+
"password": account.password,
|
|
251
|
+
"status": account.status,
|
|
252
|
+
}
|
|
253
|
+
if account.openai_password is not None:
|
|
254
|
+
meta["openai_password"] = account.openai_password
|
|
255
|
+
if account.access_token is not None:
|
|
256
|
+
meta["access_token"] = account.access_token
|
|
257
|
+
if account.workspace_id is not None:
|
|
258
|
+
meta["workspace_id"] = account.workspace_id
|
|
259
|
+
self._write_meta(account_dir, meta)
|
|
253
260
|
|
|
254
261
|
def delete_worker(self, email: str) -> None:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
with self._lock:
|
|
263
|
+
index = self._read_index(self.worker_dir)
|
|
264
|
+
info = index.pop(email, None)
|
|
265
|
+
if not info:
|
|
266
|
+
return
|
|
267
|
+
self._write_index(self.worker_dir, index)
|
|
268
|
+
account_dir = self.worker_dir / info["id"]
|
|
269
|
+
if account_dir.exists():
|
|
270
|
+
shutil.rmtree(account_dir)
|
|
263
271
|
|
|
264
272
|
def get_worker_profile_dir(self, account: WorkerAccount) -> Path:
|
|
265
273
|
profile_dir = self.worker_dir / account.id / "browser_profile"
|
package/backend/jobs.py
CHANGED
package/backend/mail/boomlify.py
CHANGED
|
@@ -133,7 +133,7 @@ class BoomlifyProvider(MailProvider):
|
|
|
133
133
|
if not email or not email_id:
|
|
134
134
|
raise MailError(f"Unexpected Boomlify create response: {raw}")
|
|
135
135
|
|
|
136
|
-
return Mailbox(email=email, password=f"
|
|
136
|
+
return Mailbox(email=email, password=f"{self.password_prefix}{email_id}")
|
|
137
137
|
|
|
138
138
|
def inbox(self, mailbox: Mailbox) -> Inbox:
|
|
139
139
|
email_id = self._extract_mailbox_id(mailbox)
|
package/backend/rpc_server.py
CHANGED
|
@@ -72,7 +72,7 @@ class RPCServer:
|
|
|
72
72
|
masked = ""
|
|
73
73
|
if value:
|
|
74
74
|
masked = value[:4] + "***" + value[-4:] if len(value) > 12 else "***"
|
|
75
|
-
items.append({"key": key, "label": label, "
|
|
75
|
+
items.append({"key": key, "label": label, "masked": masked})
|
|
76
76
|
return {"items": items, "path": str(self._settings_path())}
|
|
77
77
|
|
|
78
78
|
def _set_setting(self, key: str, value: str) -> None:
|
|
@@ -228,7 +228,9 @@ class RPCServer:
|
|
|
228
228
|
|
|
229
229
|
if m == "settings.set":
|
|
230
230
|
key = self._as_str_param(p, "key")
|
|
231
|
-
value =
|
|
231
|
+
value = p.get("value")
|
|
232
|
+
if not isinstance(value, str):
|
|
233
|
+
raise RPCError(-32602, "Invalid params", {"details": "'value' must be string"})
|
|
232
234
|
self._set_setting(key, value)
|
|
233
235
|
return make_success_response(req.request_id, {"ok": True})
|
|
234
236
|
|
|
@@ -268,10 +270,7 @@ class RPCServer:
|
|
|
268
270
|
RPCError(
|
|
269
271
|
-32000,
|
|
270
272
|
"Server error",
|
|
271
|
-
{
|
|
272
|
-
"details": str(e),
|
|
273
|
-
"traceback": tb,
|
|
274
|
-
},
|
|
273
|
+
{"details": str(e)},
|
|
275
274
|
),
|
|
276
275
|
)
|
|
277
276
|
|
package/backend/ui_facade.py
CHANGED
|
@@ -221,14 +221,24 @@ class UIFacade:
|
|
|
221
221
|
log(f"Браузер {label} открыт. Закройте окно браузера для возврата.")
|
|
222
222
|
wait_for_browser_close(context, log=log)
|
|
223
223
|
|
|
224
|
+
def _replace_manager(self, manager: SlotManager) -> None:
|
|
225
|
+
if self.manager:
|
|
226
|
+
try:
|
|
227
|
+
self.manager.close()
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
self.manager = manager
|
|
231
|
+
|
|
224
232
|
def login_admin(self, email: str, log: LogFunc) -> None:
|
|
225
|
-
|
|
226
|
-
self.manager
|
|
233
|
+
manager = SlotManager(store=self.store, admin_email=email, log=log)
|
|
234
|
+
self._replace_manager(manager)
|
|
235
|
+
manager.login_admin()
|
|
227
236
|
self.sync_codex_files()
|
|
228
237
|
|
|
229
238
|
def login_admin_manual(self, email: str, log: LogFunc) -> None:
|
|
230
|
-
|
|
231
|
-
self.manager
|
|
239
|
+
manager = SlotManager(store=self.store, admin_email=email, log=log)
|
|
240
|
+
self._replace_manager(manager)
|
|
241
|
+
manager.login_admin_manual()
|
|
232
242
|
self.sync_codex_files()
|
|
233
243
|
|
|
234
244
|
def run_slots_pipeline(
|
|
@@ -238,7 +248,8 @@ class UIFacade:
|
|
|
238
248
|
log: LogFunc,
|
|
239
249
|
progress: ProgressFunc | None = None,
|
|
240
250
|
) -> dict[str, int]:
|
|
241
|
-
|
|
251
|
+
manager = SlotManager(store=self.store, admin_email=admin_email, log=log, headless=False)
|
|
252
|
+
self._replace_manager(manager)
|
|
242
253
|
ok = 0
|
|
243
254
|
for i in range(count):
|
|
244
255
|
slot_no = i + 1
|
|
@@ -246,11 +257,11 @@ class UIFacade:
|
|
|
246
257
|
if progress:
|
|
247
258
|
progress(slot_no, count, f"slot {slot_no}/{count}")
|
|
248
259
|
try:
|
|
249
|
-
|
|
260
|
+
manager.create_invite_login_one()
|
|
250
261
|
ok += 1
|
|
251
262
|
except Exception as e:
|
|
252
263
|
log(f"Ошибка: {e}")
|
|
253
|
-
|
|
264
|
+
manager._close_admin_page()
|
|
254
265
|
log(f"Готово: {ok}/{count} слотов")
|
|
255
266
|
self.sync_codex_files()
|
|
256
267
|
return {"ok": ok, "total": count}
|
package/package.json
CHANGED
package/ui/src/menus/types.ts
CHANGED
|
@@ -375,7 +375,7 @@ export class MainScreen {
|
|
|
375
375
|
|
|
376
376
|
private async loadSettings() {
|
|
377
377
|
try {
|
|
378
|
-
const result = await this.rpc.request<{ items: Array<{ key: string; label: string;
|
|
378
|
+
const result = await this.rpc.request<{ items: Array<{ key: string; label: string; masked: string }> }>("settings.get")
|
|
379
379
|
this.state = { ...this.state, settings: result.items }
|
|
380
380
|
} catch (error) {
|
|
381
381
|
this.pushLog(`Ошибка загрузки настроек: ${String(error)}`)
|