3.11-slim: Pulling from library/python Digest: sha256:c24e9effa2821a6885165d930d939fec2af0dcf819276138f11dd45e200bd032 Status: Downloaded newer image for python:3.11-slim + cd backend + pip install flake8 pylint black isort Collecting flake8 Downloading flake8-7.3.0-py2.py3-none-any.whl.metadata (3.8 kB) Collecting pylint Downloading pylint-4.0.4-py3-none-any.whl.metadata (12 kB) Collecting black Downloading black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (86 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 86.4/86.4 kB 496.5 kB/s eta 0:00:00 Collecting isort Downloading isort-7.0.0-py3-none-any.whl.metadata (11 kB) Collecting mccabe<0.8.0,>=0.7.0 (from flake8) Downloading mccabe-0.7.0-py2.py3-none-any.whl.metadata (5.0 kB) Collecting pycodestyle<2.15.0,>=2.14.0 (from flake8) Downloading pycodestyle-2.14.0-py2.py3-none-any.whl.metadata (4.5 kB) Collecting pyflakes<3.5.0,>=3.4.0 (from flake8) Downloading pyflakes-3.4.0-py2.py3-none-any.whl.metadata (3.5 kB) Collecting astroid<=4.1.dev0,>=4.0.2 (from pylint) Downloading astroid-4.0.3-py3-none-any.whl.metadata (4.4 kB) Collecting dill>=0.3.6 (from pylint) Downloading dill-0.4.0-py3-none-any.whl.metadata (10 kB) Collecting platformdirs>=2.2 (from pylint) Downloading platformdirs-4.5.1-py3-none-any.whl.metadata (12 kB) Collecting tomlkit>=0.10.1 (from pylint) Downloading tomlkit-0.14.0-py3-none-any.whl.metadata (2.8 kB) Collecting click>=8.0.0 (from black) Downloading click-8.3.1-py3-none-any.whl.metadata (2.6 kB) Collecting mypy-extensions>=0.4.3 (from black) Downloading mypy_extensions-1.1.0-py3-none-any.whl.metadata (1.1 kB) Collecting packaging>=22.0 (from black) Downloading packaging-25.0-py3-none-any.whl.metadata (3.3 kB) Collecting pathspec>=0.9.0 (from black) Downloading pathspec-1.0.3-py3-none-any.whl.metadata (13 kB) Collecting pytokens>=0.3.0 (from black) Downloading pytokens-0.3.0-py3-none-any.whl.metadata (2.0 kB) Downloading flake8-7.3.0-py2.py3-none-any.whl (57 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 57.9/57.9 kB 688.6 kB/s eta 0:00:00 Downloading pylint-4.0.4-py3-none-any.whl (536 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 536.4/536.4 kB 2.3 MB/s eta 0:00:00 Downloading black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (1.8 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 7.8 MB/s eta 0:00:00 Downloading isort-7.0.0-py3-none-any.whl (94 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 94.7/94.7 kB 1.4 MB/s eta 0:00:00 Downloading astroid-4.0.3-py3-none-any.whl (276 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 276.4/276.4 kB 3.4 MB/s eta 0:00:00 Downloading click-8.3.1-py3-none-any.whl (108 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 108.3/108.3 kB 1.5 MB/s eta 0:00:00 Downloading dill-0.4.0-py3-none-any.whl (119 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 119.7/119.7 kB 1.7 MB/s eta 0:00:00 Downloading mccabe-0.7.0-py2.py3-none-any.whl (7.3 kB) Downloading mypy_extensions-1.1.0-py3-none-any.whl (5.0 kB) Downloading packaging-25.0-py3-none-any.whl (66 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 66.5/66.5 kB 2.2 MB/s eta 0:00:00 Downloading pathspec-1.0.3-py3-none-any.whl (55 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 55.0/55.0 kB 918.4 kB/s eta 0:00:00 Downloading platformdirs-4.5.1-py3-none-any.whl (18 kB) Downloading pycodestyle-2.14.0-py2.py3-none-any.whl (31 kB) Downloading pyflakes-3.4.0-py2.py3-none-any.whl (63 kB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 63.6/63.6 kB 2.1 MB/s eta 0:00:00 Downloading pytokens-0.3.0-py3-none-any.whl (12 kB) Downloading tomlkit-0.14.0-py3-none-any.whl (39 kB) Installing collected packages: tomlkit, pytokens, pyflakes, pycodestyle, platformdirs, pathspec, packaging, mypy-extensions, mccabe, isort, dill, click, astroid, pylint, flake8, black Successfully installed astroid-4.0.3 black-25.12.0 click-8.3.1 dill-0.4.0 flake8-7.3.0 isort-7.0.0 mccabe-0.7.0 mypy-extensions-1.1.0 packaging-25.0 pathspec-1.0.3 platformdirs-4.5.1 pycodestyle-2.14.0 pyflakes-3.4.0 pylint-4.0.4 pytokens-0.3.0 tomlkit-0.14.0 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv [notice] A new release of pip is available: 24.0 -> 25.3 [notice] To update, run: pip install --upgrade pip + echo "Running flake8..." Running flake8... + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 0 + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics ./auth.py:20:1: E302 expected 2 blank lines, found 1 ./auth.py:26:1: E302 expected 2 blank lines, found 1 ./auth.py:30:1: E302 expected 2 blank lines, found 1 ./auth.py:33:1: E302 expected 2 blank lines, found 1 ./auth.py:36:1: E302 expected 2 blank lines, found 1 ./auth.py:46:1: E302 expected 2 blank lines, found 1 ./auth.py:53:1: E302 expected 2 blank lines, found 1 ./auth.py:56:1: W293 blank line contains whitespace ./auth.py:62:1: W293 blank line contains whitespace ./auth.py:69:1: W293 blank line contains whitespace ./auth.py:76:1: W293 blank line contains whitespace ./auth.py:79:1: E302 expected 2 blank lines, found 1 ./auth.py:88:1: E302 expected 2 blank lines, found 1 ./auth.py:92:1: W293 blank line contains whitespace ./auth.py:102:1: E302 expected 2 blank lines, found 1 ./auth.py:109:1: E302 expected 2 blank lines, found 1 ./auth.py:121:1: E302 expected 2 blank lines, found 1 ./auth.py:131:1: E302 expected 2 blank lines, found 1 ./auth.py:143:1: E302 expected 2 blank lines, found 1 ./main.py:1:1: F401 'fastapi.status' imported but unused ./main.py:13:1: F401 'typing.Optional' imported but unused ./main.py:20:1: F401 'httpx' imported but unused ./main.py:22:1: F401 'oidc_config.OIDC_PROVIDERS' imported but unused ./main.py:74:1: E302 expected 2 blank lines, found 1 ./main.py:85:1: E302 expected 2 blank lines, found 1 ./main.py:91:1: E302 expected 2 blank lines, found 1 ./main.py:95:1: E302 expected 2 blank lines, found 1 ./main.py:106:1: E302 expected 2 blank lines, found 1 ./main.py:112:1: E302 expected 2 blank lines, found 1 ./main.py:118:1: E302 expected 2 blank lines, found 1 ./main.py:122:1: E305 expected 2 blank lines after class or function definition, found 1 ./main.py:125:1: E302 expected 2 blank lines, found 1 ./main.py:128:1: E302 expected 2 blank lines, found 1 ./main.py:131:1: E302 expected 2 blank lines, found 1 ./main.py:138:1: E302 expected 2 blank lines, found 1 ./main.py:141:1: W293 blank line contains whitespace ./main.py:148:1: W293 blank line contains whitespace ./main.py:152:1: W293 blank line contains whitespace ./main.py:154:1: W293 blank line contains whitespace ./main.py:158:1: W293 blank line contains whitespace ./main.py:163:1: E302 expected 2 blank lines, found 1 ./main.py:178:1: E302 expected 2 blank lines, found 1 ./main.py:190:1: E302 expected 2 blank lines, found 1 ./main.py:195:1: W293 blank line contains whitespace ./main.py:202:1: E302 expected 2 blank lines, found 1 ./main.py:207:1: W293 blank line contains whitespace ./main.py:210:1: W293 blank line contains whitespace ./main.py:213:1: W293 blank line contains whitespace ./main.py:221:1: W293 blank line contains whitespace ./main.py:224:1: W293 blank line contains whitespace ./main.py:229:1: W293 blank line contains whitespace ./main.py:233:1: W293 blank line contains whitespace ./main.py:241:1: E302 expected 2 blank lines, found 1 ./main.py:244:1: W293 blank line contains whitespace ./main.py:249:1: W293 blank line contains whitespace ./main.py:257:1: W293 blank line contains whitespace ./main.py:261:1: W293 blank line contains whitespace ./main.py:269:1: W293 blank line contains whitespace ./main.py:284:1: W293 blank line contains whitespace ./main.py:300:1: E302 expected 2 blank lines, found 1 ./main.py:305:1: W293 blank line contains whitespace ./main.py:308:1: W293 blank line contains whitespace ./main.py:311:1: W293 blank line contains whitespace ./main.py:314:1: W293 blank line contains whitespace ./main.py:332:1: W293 blank line contains whitespace ./main.py:334:1: W293 blank line contains whitespace ./main.py:343:1: E302 expected 2 blank lines, found 1 ./main.py:348:1: W293 blank line contains whitespace ./main.py:351:1: W293 blank line contains whitespace ./main.py:355:1: W293 blank line contains whitespace ./main.py:364:1: E302 expected 2 blank lines, found 1 ./main.py:368:1: W293 blank line contains whitespace ./main.py:379:1: W293 blank line contains whitespace ./main.py:388:1: E302 expected 2 blank lines, found 1 ./main.py:407:1: E302 expected 2 blank lines, found 1 ./main.py:412:1: W293 blank line contains whitespace ./main.py:418:1: W293 blank line contains whitespace ./main.py:422:1: W293 blank line contains whitespace ./main.py:430:1: W293 blank line contains whitespace ./main.py:435:1: E302 expected 2 blank lines, found 1 ./main.py:440:1: W293 blank line contains whitespace ./main.py:443:1: W293 blank line contains whitespace ./main.py:447:1: W293 blank line contains whitespace ./main.py:453:1: W293 blank line contains whitespace ./main.py:456:1: W293 blank line contains whitespace ./main.py:459:1: E302 expected 2 blank lines, found 1 ./main.py:464:1: W293 blank line contains whitespace ./main.py:467:1: W293 blank line contains whitespace ./main.py:471:1: W293 blank line contains whitespace ./main.py:475:1: W293 blank line contains whitespace ./main.py:479:1: W293 blank line contains whitespace ./main.py:482:1: W293 blank line contains whitespace ./main.py:485:1: E302 expected 2 blank lines, found 1 ./main.py:491:1: W293 blank line contains whitespace ./main.py:495:1: W293 blank line contains whitespace ./main.py:497:1: W293 blank line contains whitespace ./main.py:508:1: W293 blank line contains whitespace ./main.py:515:1: E302 expected 2 blank lines, found 1 ./main.py:520:1: W293 blank line contains whitespace ./main.py:523:1: W293 blank line contains whitespace ./main.py:527:1: W293 blank line contains whitespace ./main.py:529:1: W293 blank line contains whitespace ./main.py:533:1: W293 blank line contains whitespace ./main.py:535:1: W293 blank line contains whitespace ./main.py:541:1: W293 blank line contains whitespace ./main.py:550:1: W293 blank line contains whitespace ./main.py:554:1: W293 blank line contains whitespace ./main.py:560:1: E302 expected 2 blank lines, found 1 ./main.py:561:1: C901 'revoke_user_access' is too complex (12) ./main.py:565:1: W293 blank line contains whitespace ./main.py:569:1: W293 blank line contains whitespace ./main.py:571:1: W293 blank line contains whitespace ./main.py:575:1: W293 blank line contains whitespace ./main.py:577:1: W293 blank line contains whitespace ./main.py:603:1: W293 blank line contains whitespace ./main.py:606:1: W293 blank line contains whitespace ./main.py:612:1: E302 expected 2 blank lines, found 1 ./main.py:617:1: W293 blank line contains whitespace ./main.py:621:1: W293 blank line contains whitespace ./main.py:624:1: W293 blank line contains whitespace ./main.py:632:1: W293 blank line contains whitespace ./main.py:648:1: W293 blank line contains whitespace ./main.py:651:1: W293 blank line contains whitespace ./main.py:658:1: E302 expected 2 blank lines, found 1 ./main.py:663:1: W293 blank line contains whitespace ./main.py:666:1: W293 blank line contains whitespace ./main.py:669:1: W293 blank line contains whitespace ./main.py:671:1: W293 blank line contains whitespace ./main.py:675:1: W293 blank line contains whitespace ./main.py:679:1: W293 blank line contains whitespace ./main.py:683:1: W293 blank line contains whitespace ./main.py:688:1: W293 blank line contains whitespace ./main.py:696:1: W293 blank line contains whitespace ./main.py:701:1: W293 blank line contains whitespace ./main.py:703:1: W293 blank line contains whitespace ./main.py:706:1: W293 blank line contains whitespace ./main.py:713:1: E302 expected 2 blank lines, found 1 ./main.py:718:1: W293 blank line contains whitespace ./main.py:721:1: W293 blank line contains whitespace ./main.py:724:1: W293 blank line contains whitespace ./main.py:726:1: W293 blank line contains whitespace ./main.py:730:1: W293 blank line contains whitespace ./main.py:734:1: W293 blank line contains whitespace ./main.py:737:1: E302 expected 2 blank lines, found 1 ./main.py:742:1: W293 blank line contains whitespace ./main.py:746:1: W293 blank line contains whitespace ./main.py:760:1: W293 blank line contains whitespace ./main.py:764:1: W293 blank line contains whitespace ./main.py:771:1: W293 blank line contains whitespace ./main.py:781:1: E302 expected 2 blank lines, found 1 ./main.py:787:1: W293 blank line contains whitespace ./main.py:789:1: W293 blank line contains whitespace ./main.py:793:1: W293 blank line contains whitespace ./main.py:795:1: W293 blank line contains whitespace ./main.py:799:1: W293 blank line contains whitespace ./main.py:813:1: W293 blank line contains whitespace ./main.py:817:1: W293 blank line contains whitespace ./main.py:824:1: W293 blank line contains whitespace ./main.py:836:1: E302 expected 2 blank lines, found 1 ./main.py:842:1: W293 blank line contains whitespace ./main.py:848:1: W293 blank line contains whitespace ./main.py:850:1: W293 blank line contains whitespace ./main.py:858:1: W293 blank line contains whitespace ./main.py:869:1: E302 expected 2 blank lines, found 1 ./main.py:874:1: W293 blank line contains whitespace ./main.py:878:1: W293 blank line contains whitespace ./main.py:880:1: W293 blank line contains whitespace ./main.py:888:1: W293 blank line contains whitespace ./main.py:898:1: W293 blank line contains whitespace ./main.py:901:1: E302 expected 2 blank lines, found 1 ./main.py:905:1: W293 blank line contains whitespace ./main.py:909:1: W293 blank line contains whitespace ./main.py:914:1: E302 expected 2 blank lines, found 1 ./main.py:918:1: W293 blank line contains whitespace ./main.py:922:1: W293 blank line contains whitespace ./main.py:925:1: W293 blank line contains whitespace ./main.py:929:1: E302 expected 2 blank lines, found 1 ./main.py:933:1: W293 blank line contains whitespace ./main.py:937:1: W293 blank line contains whitespace ./main.py:940:1: W293 blank line contains whitespace ./main.py:945:1: C901 'read_server_output' is too complex (11) ./main.py:945:1: E302 expected 2 blank lines, found 1 ./main.py:949:1: W293 blank line contains whitespace ./main.py:954:1: W293 blank line contains whitespace ./main.py:959:1: W293 blank line contains whitespace ./main.py:965:1: W293 blank line contains whitespace ./main.py:971:1: W293 blank line contains whitespace ./main.py:980:1: E302 expected 2 blank lines, found 1 ./main.py:984:1: W293 blank line contains whitespace ./main.py:988:1: W293 blank line contains whitespace ./main.py:991:1: W293 blank line contains whitespace ./main.py:994:1: W293 blank line contains whitespace ./main.py:996:1: W293 blank line contains whitespace ./main.py:1019:1: W293 blank line contains whitespace ./main.py:1022:1: W293 blank line contains whitespace ./main.py:1024:1: W293 blank line contains whitespace ./main.py:1031:1: E302 expected 2 blank lines, found 1 ./main.py:1035:1: W293 blank line contains whitespace ./main.py:1038:1: W293 blank line contains whitespace ./main.py:1040:1: W293 blank line contains whitespace ./main.py:1045:1: W293 blank line contains whitespace ./main.py:1057:9: E722 do not use bare 'except' ./main.py:1063:1: W293 blank line contains whitespace ./main.py:1066:1: E302 expected 2 blank lines, found 1 ./main.py:1070:1: W293 blank line contains whitespace ./main.py:1073:1: W293 blank line contains whitespace ./main.py:1075:1: W293 blank line contains whitespace ./main.py:1079:1: W293 blank line contains whitespace ./main.py:1093:1: E302 expected 2 blank lines, found 1 ./main.py:1097:1: W293 blank line contains whitespace ./main.py:1099:1: W293 blank line contains whitespace ./main.py:1103:5: E722 do not use bare 'except' ./main.py:1105:1: W293 blank line contains whitespace ./main.py:1113:1: W293 blank line contains whitespace ./main.py:1124:1: W293 blank line contains whitespace ./main.py:1128:1: W293 blank line contains whitespace ./main.py:1153:1: E302 expected 2 blank lines, found 1 ./main.py:1157:1: W293 blank line contains whitespace ./main.py:1165:1: W293 blank line contains whitespace ./main.py:1167:1: W293 blank line contains whitespace ./main.py:1182:1: E302 expected 2 blank lines, found 1 ./main.py:1183:1: C901 'list_files' is too complex (11) ./main.py:1186:1: W293 blank line contains whitespace ./main.py:1190:1: W293 blank line contains whitespace ./main.py:1192:1: W293 blank line contains whitespace ./main.py:1198:5: E722 do not use bare 'except' ./main.py:1200:1: W293 blank line contains whitespace ./main.py:1203:1: W293 blank line contains whitespace ./main.py:1206:1: W293 blank line contains whitespace ./main.py:1218:1: W293 blank line contains whitespace ./main.py:1221:1: E302 expected 2 blank lines, found 1 ./main.py:1225:1: W293 blank line contains whitespace ./main.py:1228:1: W293 blank line contains whitespace ./main.py:1231:1: W293 blank line contains whitespace ./main.py:1234:1: E302 expected 2 blank lines, found 1 ./main.py:1237:1: W293 blank line contains whitespace ./main.py:1240:1: W293 blank line contains whitespace ./main.py:1243:1: W293 blank line contains whitespace ./main.py:1247:1: W293 blank line contains whitespace ./main.py:1250:1: W293 blank line contains whitespace ./main.py:1257:1: W293 blank line contains whitespace ./main.py:1266:1: W293 blank line contains whitespace ./main.py:1269:1: E302 expected 2 blank lines, found 1 ./main.py:1274:1: W293 blank line contains whitespace ./main.py:1278:1: W293 blank line contains whitespace ./main.py:1281:1: W293 blank line contains whitespace ./main.py:1284:1: W293 blank line contains whitespace ./main.py:1286:1: W293 blank line contains whitespace ./main.py:1292:1: W293 blank line contains whitespace ./main.py:1294:1: W293 blank line contains whitespace ./main.py:1298:1: W293 blank line contains whitespace ./main.py:1312:1: W293 blank line contains whitespace ./main.py:1318:1: E302 expected 2 blank lines, found 1 ./main.py:1322:1: W293 blank line contains whitespace ./main.py:1325:1: W293 blank line contains whitespace ./main.py:1328:1: W293 blank line contains whitespace ./main.py:1333:1: W293 blank line contains whitespace ./main.py:1336:1: E302 expected 2 blank lines, found 1 ./main.py:1340:1: W293 blank line contains whitespace ./main.py:1343:1: W293 blank line contains whitespace ./main.py:1346:1: W293 blank line contains whitespace ./main.py:1354:1: E302 expected 2 blank lines, found 1 ./main.py:1358:1: W293 blank line contains whitespace ./main.py:1361:1: W293 blank line contains whitespace ./main.py:1364:1: W293 blank line contains whitespace ./main.py:1372:1: E302 expected 2 blank lines, found 1 ./main.py:1376:1: W293 blank line contains whitespace ./main.py:1379:1: W293 blank line contains whitespace ./main.py:1382:1: W293 blank line contains whitespace ./main.py:1384:1: W293 blank line contains whitespace ./main.py:1387:1: W293 blank line contains whitespace ./main.py:1390:1: W293 blank line contains whitespace ./main.py:1394:1: E302 expected 2 blank lines, found 1 ./main.py:1399:1: W293 blank line contains whitespace ./main.py:1402:1: W293 blank line contains whitespace ./main.py:1405:1: W293 blank line contains whitespace ./main.py:1408:1: W293 blank line contains whitespace ./main.py:1418:1: W293 blank line contains whitespace ./main.py:1420:1: W293 blank line contains whitespace ./main.py:1424:1: W293 blank line contains whitespace ./main.py:1427:1: W293 blank line contains whitespace ./main.py:1430:1: W293 blank line contains whitespace ./main.py:1433:1: W293 blank line contains whitespace ./main.py:1437:1: W293 blank line contains whitespace ./main.py:1441:1: W293 blank line contains whitespace ./main.py:1448:1: E302 expected 2 blank lines, found 1 ./main.py:1449:1: F811 redefinition of unused 'rename_file' from line 1373 ./main.py:1452:1: W293 blank line contains whitespace ./main.py:1455:1: W293 blank line contains whitespace ./main.py:1458:1: W293 blank line contains whitespace ./main.py:1460:1: W293 blank line contains whitespace ./main.py:1463:1: W293 blank line contains whitespace ./main.py:1466:1: W293 blank line contains whitespace ./main.py:1471:1: E302 expected 2 blank lines, found 1 ./main.py:1477:1: W293 blank line contains whitespace ./main.py:1479:1: W293 blank line contains whitespace ./main.py:1483:1: W293 blank line contains whitespace ./main.py:1488:1: E302 expected 2 blank lines, found 1 ./main.py:1494:1: W293 blank line contains whitespace ./main.py:1496:1: W293 blank line contains whitespace ./main.py:1499:1: W293 blank line contains whitespace ./main.py:1516:1: W293 blank line contains whitespace ./main.py:1519:1: W293 blank line contains whitespace ./main.py:1522:1: E302 expected 2 blank lines, found 1 ./main.py:1528:1: W293 blank line contains whitespace ./main.py:1530:1: W293 blank line contains whitespace ./main.py:1533:1: W293 blank line contains whitespace ./main.py:1535:1: W293 blank line contains whitespace ./main.py:1539:1: W293 blank line contains whitespace ./main.py:1542:1: E302 expected 2 blank lines, found 1 ./main.py:1548:1: W293 blank line contains whitespace ./main.py:1550:1: W293 blank line contains whitespace ./main.py:1553:1: W293 blank line contains whitespace ./main.py:1555:1: W293 blank line contains whitespace ./main.py:1559:1: W293 blank line contains whitespace ./main.py:1565:1: W293 blank line contains whitespace ./main.py:1568:1: W293 blank line contains whitespace ./main.py:1571:1: W293 blank line contains whitespace ./main.py:1574:1: E302 expected 2 blank lines, found 1 ./main.py:1579:1: W293 blank line contains whitespace ./main.py:1583:1: W293 blank line contains whitespace ./main.py:1585:1: W293 blank line contains whitespace ./main.py:1588:1: W293 blank line contains whitespace ./main.py:1592:1: W293 blank line contains whitespace ./main.py:1596:1: W293 blank line contains whitespace ./main.py:1603:1: W293 blank line contains whitespace ./main.py:1609:1: W293 blank line contains whitespace ./main.py:1611:1: W293 blank line contains whitespace ./main.py:1614:1: W293 blank line contains whitespace ./main.py:1630:1: E302 expected 2 blank lines, found 1 ./main.py:1635:1: E302 expected 2 blank lines, found 1 ./main.py:1639:1: E302 expected 2 blank lines, found 1 ./main.py:1644:1: E302 expected 2 blank lines, found 1 ./main.py:1645:1: F811 redefinition of unused 'get_users' from line 389 ./main.py:1647:1: W293 blank line contains whitespace ./main.py:1654:1: W293 blank line contains whitespace ./main.py:1658:1: E302 expected 2 blank lines, found 1 ./main.py:1661:1: E302 expected 2 blank lines, found 1 ./main.py:1664:1: W293 blank line contains whitespace ./main.py:1666:1: W293 blank line contains whitespace ./main.py:1669:1: W293 blank line contains whitespace ./main.py:1672:1: W293 blank line contains whitespace ./main.py:1675:53: F541 f-string is missing placeholders ./main.py:1676:1: W293 blank line contains whitespace ./main.py:1679:1: W293 blank line contains whitespace ./main.py:1682:1: W293 blank line contains whitespace ./main.py:1714:1: W293 blank line contains whitespace ./main.py:1716:1: W293 blank line contains whitespace ./main.py:1717:128: E501 line too long (129 > 127 characters) ./main.py:1720:1: E302 expected 2 blank lines, found 1 ./main.py:1723:1: E302 expected 2 blank lines, found 1 ./main.py:1726:1: W293 blank line contains whitespace ./main.py:1728:1: W293 blank line contains whitespace ./main.py:1731:1: W293 blank line contains whitespace ./main.py:1734:1: W293 blank line contains whitespace ./main.py:1739:128: E501 line too long (140 > 127 characters) ./main.py:1740:1: W293 blank line contains whitespace ./main.py:1748:1: W293 blank line contains whitespace ./main.py:1750:1: W293 blank line contains whitespace ./main.py:1754:1: E302 expected 2 blank lines, found 1 ./main.py:1757:1: W293 blank line contains whitespace ./main.py:1759:1: W293 blank line contains whitespace ./main.py:1762:1: W293 blank line contains whitespace ./main.py:1765:1: W293 blank line contains whitespace ./main.py:1773:1: W293 blank line contains whitespace ./main.py:1775:1: W293 blank line contains whitespace ./main.py:1779:1: E302 expected 2 blank lines, found 1 ./main.py:1780:1: F811 redefinition of unused 'delete_user' from line 436 ./main.py:1782:1: W293 blank line contains whitespace ./main.py:1784:1: W293 blank line contains whitespace ./main.py:1787:1: W293 blank line contains whitespace ./main.py:1790:1: W293 blank line contains whitespace ./main.py:1795:128: E501 line too long (134 > 127 characters) ./main.py:1796:1: W293 blank line contains whitespace ./main.py:1799:1: W293 blank line contains whitespace ./main.py:1803:1: E302 expected 2 blank lines, found 1 ./main.py:1806:1: E302 expected 2 blank lines, found 1 ./main.py:1809:1: W293 blank line contains whitespace ./main.py:1811:1: W293 blank line contains whitespace ./main.py:1814:1: W293 blank line contains whitespace ./main.py:1817:1: W293 blank line contains whitespace ./main.py:1820:1: W293 blank line contains whitespace ./main.py:1826:1: W293 blank line contains whitespace ./main.py:1828:1: W293 blank line contains whitespace ./main.py:1832:1: E302 expected 2 blank lines, found 1 ./main.py:1835:1: W293 blank line contains whitespace ./main.py:1837:1: W293 blank line contains whitespace ./main.py:1840:1: W293 blank line contains whitespace ./main.py:1844:1: W293 blank line contains whitespace ./main.py:1848:1: W293 blank line contains whitespace ./main.py:1850:1: W293 blank line contains whitespace ./main.py:1854:1: E302 expected 2 blank lines, found 1 ./main.py:1857:1: E302 expected 2 blank lines, found 1 ./main.py:1858:1: F811 redefinition of unused 'update_user_permissions' from line 516 ./main.py:1860:1: W293 blank line contains whitespace ./main.py:1862:1: W293 blank line contains whitespace ./main.py:1865:1: W293 blank line contains whitespace ./main.py:1868:1: W293 blank line contains whitespace ./migrate_users.py:11:1: C901 'migrate_users' is too complex (22) ./migrate_users.py:11:1: E302 expected 2 blank lines, found 1 ./migrate_users.py:13:1: W293 blank line contains whitespace ./migrate_users.py:15:1: W293 blank line contains whitespace ./migrate_users.py:21:1: W293 blank line contains whitespace ./migrate_users.py:33:1: W293 blank line contains whitespace ./migrate_users.py:44:1: W293 blank line contains whitespace ./migrate_users.py:59:1: W293 blank line contains whitespace ./migrate_users.py:63:1: W293 blank line contains whitespace ./migrate_users.py:66:1: W293 blank line contains whitespace ./migrate_users.py:87:1: W293 blank line contains whitespace ./migrate_users.py:92:1: W293 blank line contains whitespace ./migrate_users.py:95:1: W293 blank line contains whitespace ./migrate_users.py:99:19: F541 f-string is missing placeholders ./migrate_users.py:100:1: W293 blank line contains whitespace ./migrate_users.py:147:1: W293 blank line contains whitespace ./migrate_users.py:156:1: W293 blank line contains whitespace ./migrate_users.py:168:1: W293 blank line contains whitespace ./migrate_users.py:180:1: E302 expected 2 blank lines, found 1 ./migrate_users.py:183:1: W293 blank line contains whitespace ./migrate_users.py:187:1: W293 blank line contains whitespace ./migrate_users.py:194:1: W293 blank line contains whitespace ./migrate_users.py:200:1: W293 blank line contains whitespace ./migrate_users.py:204:1: W293 blank line contains whitespace ./migrate_users.py:208:15: F541 f-string is missing placeholders ./migrate_users.py:212:1: W293 blank line contains whitespace ./migrate_users.py:220:1: E305 expected 2 blank lines after class or function definition, found 1 ./migrate_users.py:224:1: W293 blank line contains whitespace ./migrate_users.py:227:1: W293 blank line contains whitespace ./migrate_users.py:231:1: W293 blank line contains whitespace ./models.py:2:1: F401 'typing.Optional' imported but unused ./models.py:4:1: E302 expected 2 blank lines, found 1 ./models.py:8:1: E302 expected 2 blank lines, found 1 ./models.py:12:1: E302 expected 2 blank lines, found 1 ./models.py:18:1: E302 expected 2 blank lines, found 1 ./models.py:22:1: E302 expected 2 blank lines, found 1 ./oidc_config.py:21:1: E302 expected 2 blank lines, found 1 ./oidc_config.py:29:1: E302 expected 2 blank lines, found 1 ./oidc_config.py:31:62: W292 no newline at end of file ./user_management_endpoints.py:8:1: F401 'typing.Optional' imported but unused ./user_management_endpoints.py:8:1: F401 'typing.List' imported but unused ./user_management_endpoints.py:15:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:18:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:21:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:24:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:28:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:32:1: W293 blank line contains whitespace ./user_management_endpoints.py:37:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:42:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:46:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:51:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:54:1: W293 blank line contains whitespace ./user_management_endpoints.py:56:1: W293 blank line contains whitespace ./user_management_endpoints.py:63:1: W293 blank line contains whitespace ./user_management_endpoints.py:67:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:68:1: C901 'change_user_role' is too complex (11) ./user_management_endpoints.py:70:1: W293 blank line contains whitespace ./user_management_endpoints.py:72:1: W293 blank line contains whitespace ./user_management_endpoints.py:75:1: W293 blank line contains whitespace ./user_management_endpoints.py:78:1: W293 blank line contains whitespace ./user_management_endpoints.py:83:1: W293 blank line contains whitespace ./user_management_endpoints.py:89:1: W293 blank line contains whitespace ./user_management_endpoints.py:93:1: W293 blank line contains whitespace ./user_management_endpoints.py:145:1: W293 blank line contains whitespace ./user_management_endpoints.py:147:1: W293 blank line contains whitespace ./user_management_endpoints.py:157:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:160:1: W293 blank line contains whitespace ./user_management_endpoints.py:162:1: W293 blank line contains whitespace ./user_management_endpoints.py:165:1: W293 blank line contains whitespace ./user_management_endpoints.py:168:1: W293 blank line contains whitespace ./user_management_endpoints.py:175:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:178:1: W293 blank line contains whitespace ./user_management_endpoints.py:180:1: W293 blank line contains whitespace ./user_management_endpoints.py:183:1: W293 blank line contains whitespace ./user_management_endpoints.py:186:1: W293 blank line contains whitespace ./user_management_endpoints.py:189:1: W293 blank line contains whitespace ./user_management_endpoints.py:195:1: W293 blank line contains whitespace ./user_management_endpoints.py:197:1: W293 blank line contains whitespace ./user_management_endpoints.py:205:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:208:1: W293 blank line contains whitespace ./user_management_endpoints.py:210:1: W293 blank line contains whitespace ./user_management_endpoints.py:213:1: W293 blank line contains whitespace ./user_management_endpoints.py:217:1: W293 blank line contains whitespace ./user_management_endpoints.py:221:1: W293 blank line contains whitespace ./user_management_endpoints.py:223:1: W293 blank line contains whitespace ./user_management_endpoints.py:231:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:234:1: W293 blank line contains whitespace ./user_management_endpoints.py:236:1: W293 blank line contains whitespace ./user_management_endpoints.py:239:1: W293 blank line contains whitespace ./user_management_endpoints.py:242:1: W293 blank line contains whitespace ./user_management_endpoints.py:245:1: W293 blank line contains whitespace ./user_management_endpoints.py:248:1: W293 blank line contains whitespace ./user_management_endpoints.py:255:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:258:1: W293 blank line contains whitespace ./user_management_endpoints.py:260:1: W293 blank line contains whitespace ./user_management_endpoints.py:263:1: W293 blank line contains whitespace ./user_management_endpoints.py:266:1: W293 blank line contains whitespace ./user_management_endpoints.py:269:1: W293 blank line contains whitespace ./user_management_endpoints.py:281:1: W293 blank line contains whitespace ./user_management_endpoints.py:283:1: W293 blank line contains whitespace ./user_management_endpoints.py:291:1: E302 expected 2 blank lines, found 1 ./user_management_endpoints.py:294:1: W293 blank line contains whitespace ./user_management_endpoints.py:296:1: W293 blank line contains whitespace ./user_management_endpoints.py:299:1: W293 blank line contains whitespace ./user_management_endpoints.py:302:1: W293 blank line contains whitespace ./user_management_endpoints.py:314:1: W293 blank line contains whitespace ./user_management_endpoints.py:316:1: W293 blank line contains whitespace 5 C901 'revoke_user_access' is too complex (12) 111 E302 expected 2 blank lines, found 1 2 E305 expected 2 blank lines after class or function definition, found 1 3 E501 line too long (129 > 127 characters) 3 E722 do not use bare 'except' 7 F401 'fastapi.status' imported but unused 3 F541 f-string is missing placeholders 4 F811 redefinition of unused 'rename_file' from line 1373 1 W292 no newline at end of file 366 W293 blank line contains whitespace 505 + echo "Running pylint..." Running pylint... + pylint **/*.py --exit-zero --max-line-length=127 ************* Module migrate_users migrate_users.py:13:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:15:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:21:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:33:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:44:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:59:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:63:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:66:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:87:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:92:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:95:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:100:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:147:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:156:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:168:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:183:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:187:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:194:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:200:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:204:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:212:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:224:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:227:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:231:0: C0303: Trailing whitespace (trailing-whitespace) migrate_users.py:30:11: W0718: Catching too general exception Exception (broad-exception-caught) migrate_users.py:41:11: W0718: Catching too general exception Exception (broad-exception-caught) migrate_users.py:99:18: W1309: Using an f-string that does not have any interpolated variables (f-string-without-interpolation) migrate_users.py:175:11: W0718: Catching too general exception Exception (broad-exception-caught) migrate_users.py:11:0: R0911: Too many return statements (8/6) (too-many-return-statements) migrate_users.py:11:0: R0912: Too many branches (21/12) (too-many-branches) migrate_users.py:11:0: R0915: Too many statements (82/50) (too-many-statements) migrate_users.py:191:11: W0718: Catching too general exception Exception (broad-exception-caught) migrate_users.py:208:14: W1309: Using an f-string that does not have any interpolated variables (f-string-without-interpolation) migrate_users.py:226:4: C0103: Constant name "success" doesn't conform to UPPER_CASE naming style (invalid-name) ************* Module user_management_endpoints user_management_endpoints.py:32:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:54:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:56:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:63:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:70:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:72:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:75:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:78:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:83:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:89:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:93:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:145:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:147:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:160:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:162:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:165:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:168:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:178:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:180:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:183:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:186:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:189:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:195:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:197:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:208:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:210:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:213:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:217:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:221:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:223:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:234:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:236:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:239:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:242:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:245:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:248:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:258:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:260:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:263:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:266:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:269:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:281:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:283:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:294:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:296:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:299:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:302:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:314:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:316:0: C0303: Trailing whitespace (trailing-whitespace) user_management_endpoints.py:6:0: E0401: Unable to import 'fastapi' (import-error) user_management_endpoints.py:7:0: E0401: Unable to import 'pydantic' (import-error) user_management_endpoints.py:15:0: C0115: Missing class docstring (missing-class-docstring) user_management_endpoints.py:15:0: R0903: Too few public methods (0/2) (too-few-public-methods) user_management_endpoints.py:18:0: C0115: Missing class docstring (missing-class-docstring) user_management_endpoints.py:18:0: R0903: Too few public methods (0/2) (too-few-public-methods) user_management_endpoints.py:21:0: C0115: Missing class docstring (missing-class-docstring) user_management_endpoints.py:21:0: R0903: Too few public methods (0/2) (too-few-public-methods) user_management_endpoints.py:24:0: C0115: Missing class docstring (missing-class-docstring) user_management_endpoints.py:24:0: R0903: Too few public methods (0/2) (too-few-public-methods) user_management_endpoints.py:28:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:37:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:42:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:46:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:52:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:59:8: W0612: Unused variable 'username' (unused-variable) user_management_endpoints.py:68:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:158:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:176:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:206:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:232:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:256:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:292:0: C0116: Missing function or method docstring (missing-function-docstring) user_management_endpoints.py:8:0: C0411: standard import "typing.Optional" should be placed before third party imports "fastapi.APIRouter", "pydantic.BaseModel" (wrong-import-order) user_management_endpoints.py:9:0: C0411: standard import "json" should be placed before third party imports "fastapi.APIRouter", "pydantic.BaseModel" (wrong-import-order) user_management_endpoints.py:10:0: C0411: standard import "pathlib.Path" should be placed before third party imports "fastapi.APIRouter", "pydantic.BaseModel" (wrong-import-order) user_management_endpoints.py:8:0: W0611: Unused Optional imported from typing (unused-import) user_management_endpoints.py:8:0: W0611: Unused List imported from typing (unused-import) ************* Module oidc_config oidc_config.py:31:0: C0304: Final newline missing (missing-final-newline) ************* Module models models.py:1:0: C0114: Missing module docstring (missing-module-docstring) models.py:1:0: E0401: Unable to import 'pydantic' (import-error) models.py:4:0: C0115: Missing class docstring (missing-class-docstring) models.py:4:0: R0903: Too few public methods (0/2) (too-few-public-methods) models.py:8:0: C0115: Missing class docstring (missing-class-docstring) models.py:8:0: R0903: Too few public methods (0/2) (too-few-public-methods) models.py:12:0: C0115: Missing class docstring (missing-class-docstring) models.py:12:0: R0903: Too few public methods (0/2) (too-few-public-methods) models.py:18:0: C0115: Missing class docstring (missing-class-docstring) models.py:18:0: R0903: Too few public methods (0/2) (too-few-public-methods) models.py:22:0: C0115: Missing class docstring (missing-class-docstring) models.py:22:0: R0903: Too few public methods (0/2) (too-few-public-methods) models.py:2:0: C0411: standard import "typing.Optional" should be placed before third party import "pydantic.BaseModel" (wrong-import-order) models.py:2:0: W0611: Unused Optional imported from typing (unused-import) ************* Module main main.py:141:0: C0303: Trailing whitespace (trailing-whitespace) main.py:148:0: C0303: Trailing whitespace (trailing-whitespace) main.py:152:0: C0303: Trailing whitespace (trailing-whitespace) main.py:154:0: C0303: Trailing whitespace (trailing-whitespace) main.py:158:0: C0303: Trailing whitespace (trailing-whitespace) main.py:195:0: C0303: Trailing whitespace (trailing-whitespace) main.py:207:0: C0303: Trailing whitespace (trailing-whitespace) main.py:210:0: C0303: Trailing whitespace (trailing-whitespace) main.py:213:0: C0303: Trailing whitespace (trailing-whitespace) main.py:221:0: C0303: Trailing whitespace (trailing-whitespace) main.py:224:0: C0303: Trailing whitespace (trailing-whitespace) main.py:229:0: C0303: Trailing whitespace (trailing-whitespace) main.py:233:0: C0303: Trailing whitespace (trailing-whitespace) main.py:244:0: C0303: Trailing whitespace (trailing-whitespace) main.py:249:0: C0303: Trailing whitespace (trailing-whitespace) main.py:257:0: C0303: Trailing whitespace (trailing-whitespace) main.py:261:0: C0303: Trailing whitespace (trailing-whitespace) main.py:269:0: C0303: Trailing whitespace (trailing-whitespace) main.py:284:0: C0303: Trailing whitespace (trailing-whitespace) main.py:305:0: C0303: Trailing whitespace (trailing-whitespace) main.py:308:0: C0303: Trailing whitespace (trailing-whitespace) main.py:311:0: C0303: Trailing whitespace (trailing-whitespace) main.py:314:0: C0303: Trailing whitespace (trailing-whitespace) main.py:332:0: C0303: Trailing whitespace (trailing-whitespace) main.py:334:0: C0303: Trailing whitespace (trailing-whitespace) main.py:348:0: C0303: Trailing whitespace (trailing-whitespace) main.py:351:0: C0303: Trailing whitespace (trailing-whitespace) main.py:355:0: C0303: Trailing whitespace (trailing-whitespace) main.py:368:0: C0303: Trailing whitespace (trailing-whitespace) main.py:379:0: C0303: Trailing whitespace (trailing-whitespace) main.py:412:0: C0303: Trailing whitespace (trailing-whitespace) main.py:418:0: C0303: Trailing whitespace (trailing-whitespace) main.py:422:0: C0303: Trailing whitespace (trailing-whitespace) main.py:430:0: C0303: Trailing whitespace (trailing-whitespace) main.py:440:0: C0303: Trailing whitespace (trailing-whitespace) main.py:443:0: C0303: Trailing whitespace (trailing-whitespace) main.py:447:0: C0303: Trailing whitespace (trailing-whitespace) main.py:453:0: C0303: Trailing whitespace (trailing-whitespace) main.py:456:0: C0303: Trailing whitespace (trailing-whitespace) main.py:464:0: C0303: Trailing whitespace (trailing-whitespace) main.py:467:0: C0303: Trailing whitespace (trailing-whitespace) main.py:471:0: C0303: Trailing whitespace (trailing-whitespace) main.py:475:0: C0303: Trailing whitespace (trailing-whitespace) main.py:479:0: C0303: Trailing whitespace (trailing-whitespace) main.py:482:0: C0303: Trailing whitespace (trailing-whitespace) main.py:491:0: C0303: Trailing whitespace (trailing-whitespace) main.py:495:0: C0303: Trailing whitespace (trailing-whitespace) main.py:497:0: C0303: Trailing whitespace (trailing-whitespace) main.py:508:0: C0303: Trailing whitespace (trailing-whitespace) main.py:520:0: C0303: Trailing whitespace (trailing-whitespace) main.py:523:0: C0303: Trailing whitespace (trailing-whitespace) main.py:527:0: C0303: Trailing whitespace (trailing-whitespace) main.py:529:0: C0303: Trailing whitespace (trailing-whitespace) main.py:533:0: C0303: Trailing whitespace (trailing-whitespace) main.py:535:0: C0303: Trailing whitespace (trailing-whitespace) main.py:541:0: C0303: Trailing whitespace (trailing-whitespace) main.py:550:0: C0303: Trailing whitespace (trailing-whitespace) main.py:554:0: C0303: Trailing whitespace (trailing-whitespace) main.py:565:0: C0303: Trailing whitespace (trailing-whitespace) main.py:569:0: C0303: Trailing whitespace (trailing-whitespace) main.py:571:0: C0303: Trailing whitespace (trailing-whitespace) main.py:575:0: C0303: Trailing whitespace (trailing-whitespace) main.py:577:0: C0303: Trailing whitespace (trailing-whitespace) main.py:603:0: C0303: Trailing whitespace (trailing-whitespace) main.py:606:0: C0303: Trailing whitespace (trailing-whitespace) main.py:617:0: C0303: Trailing whitespace (trailing-whitespace) main.py:621:0: C0303: Trailing whitespace (trailing-whitespace) main.py:624:0: C0303: Trailing whitespace (trailing-whitespace) main.py:632:0: C0303: Trailing whitespace (trailing-whitespace) main.py:648:0: C0303: Trailing whitespace (trailing-whitespace) main.py:651:0: C0303: Trailing whitespace (trailing-whitespace) main.py:663:0: C0303: Trailing whitespace (trailing-whitespace) main.py:666:0: C0303: Trailing whitespace (trailing-whitespace) main.py:669:0: C0303: Trailing whitespace (trailing-whitespace) main.py:671:0: C0303: Trailing whitespace (trailing-whitespace) main.py:675:0: C0303: Trailing whitespace (trailing-whitespace) main.py:679:0: C0303: Trailing whitespace (trailing-whitespace) main.py:683:0: C0303: Trailing whitespace (trailing-whitespace) main.py:688:0: C0303: Trailing whitespace (trailing-whitespace) main.py:696:0: C0303: Trailing whitespace (trailing-whitespace) main.py:701:0: C0303: Trailing whitespace (trailing-whitespace) main.py:703:0: C0303: Trailing whitespace (trailing-whitespace) main.py:706:0: C0303: Trailing whitespace (trailing-whitespace) main.py:718:0: C0303: Trailing whitespace (trailing-whitespace) main.py:721:0: C0303: Trailing whitespace (trailing-whitespace) main.py:724:0: C0303: Trailing whitespace (trailing-whitespace) main.py:726:0: C0303: Trailing whitespace (trailing-whitespace) main.py:730:0: C0303: Trailing whitespace (trailing-whitespace) main.py:734:0: C0303: Trailing whitespace (trailing-whitespace) main.py:742:0: C0303: Trailing whitespace (trailing-whitespace) main.py:746:0: C0303: Trailing whitespace (trailing-whitespace) main.py:760:0: C0303: Trailing whitespace (trailing-whitespace) main.py:764:0: C0303: Trailing whitespace (trailing-whitespace) main.py:771:0: C0303: Trailing whitespace (trailing-whitespace) main.py:787:0: C0303: Trailing whitespace (trailing-whitespace) main.py:789:0: C0303: Trailing whitespace (trailing-whitespace) main.py:793:0: C0303: Trailing whitespace (trailing-whitespace) main.py:795:0: C0303: Trailing whitespace (trailing-whitespace) main.py:799:0: C0303: Trailing whitespace (trailing-whitespace) main.py:813:0: C0303: Trailing whitespace (trailing-whitespace) main.py:817:0: C0303: Trailing whitespace (trailing-whitespace) main.py:824:0: C0303: Trailing whitespace (trailing-whitespace) main.py:842:0: C0303: Trailing whitespace (trailing-whitespace) main.py:848:0: C0303: Trailing whitespace (trailing-whitespace) main.py:850:0: C0303: Trailing whitespace (trailing-whitespace) main.py:858:0: C0303: Trailing whitespace (trailing-whitespace) main.py:874:0: C0303: Trailing whitespace (trailing-whitespace) main.py:878:0: C0303: Trailing whitespace (trailing-whitespace) main.py:880:0: C0303: Trailing whitespace (trailing-whitespace) main.py:888:0: C0303: Trailing whitespace (trailing-whitespace) main.py:898:0: C0303: Trailing whitespace (trailing-whitespace) main.py:905:0: C0303: Trailing whitespace (trailing-whitespace) main.py:909:0: C0303: Trailing whitespace (trailing-whitespace) main.py:918:0: C0303: Trailing whitespace (trailing-whitespace) main.py:922:0: C0303: Trailing whitespace (trailing-whitespace) main.py:925:0: C0303: Trailing whitespace (trailing-whitespace) main.py:933:0: C0303: Trailing whitespace (trailing-whitespace) main.py:937:0: C0303: Trailing whitespace (trailing-whitespace) main.py:940:0: C0303: Trailing whitespace (trailing-whitespace) main.py:949:0: C0303: Trailing whitespace (trailing-whitespace) main.py:954:0: C0303: Trailing whitespace (trailing-whitespace) main.py:959:0: C0303: Trailing whitespace (trailing-whitespace) main.py:965:0: C0303: Trailing whitespace (trailing-whitespace) main.py:971:0: C0303: Trailing whitespace (trailing-whitespace) main.py:984:0: C0303: Trailing whitespace (trailing-whitespace) main.py:988:0: C0303: Trailing whitespace (trailing-whitespace) main.py:991:0: C0303: Trailing whitespace (trailing-whitespace) main.py:994:0: C0303: Trailing whitespace (trailing-whitespace) main.py:996:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1019:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1022:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1024:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1035:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1038:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1040:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1045:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1063:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1070:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1073:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1075:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1079:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1097:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1099:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1105:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1113:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1124:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1128:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1157:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1165:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1167:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1186:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1190:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1192:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1200:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1203:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1206:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1218:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1225:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1228:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1231:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1237:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1240:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1243:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1247:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1250:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1257:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1266:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1274:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1278:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1281:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1284:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1286:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1292:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1294:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1298:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1312:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1322:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1325:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1328:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1333:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1340:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1343:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1346:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1358:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1361:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1364:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1376:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1379:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1382:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1384:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1387:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1390:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1399:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1402:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1405:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1408:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1418:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1420:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1424:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1427:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1430:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1433:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1437:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1441:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1452:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1455:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1458:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1460:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1463:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1466:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1477:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1479:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1483:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1494:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1496:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1499:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1516:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1519:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1528:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1530:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1533:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1535:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1539:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1548:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1550:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1553:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1555:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1559:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1565:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1568:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1571:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1579:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1583:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1585:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1588:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1592:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1596:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1603:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1609:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1611:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1614:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1647:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1654:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1664:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1666:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1669:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1672:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1676:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1679:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1682:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1714:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1716:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1717:0: C0301: Line too long (129/127) (line-too-long) main.py:1726:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1728:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1731:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1734:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1739:0: C0301: Line too long (140/127) (line-too-long) main.py:1740:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1748:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1750:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1757:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1759:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1762:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1765:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1773:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1775:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1782:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1784:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1787:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1790:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1795:0: C0301: Line too long (134/127) (line-too-long) main.py:1796:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1799:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1809:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1811:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1814:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1817:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1820:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1826:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1828:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1835:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1837:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1840:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1844:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1848:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1850:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1860:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1862:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1865:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1868:0: C0303: Trailing whitespace (trailing-whitespace) main.py:1:0: C0302: Too many lines in module (1874/1000) (too-many-lines) main.py:1:0: C0114: Missing module docstring (missing-module-docstring) main.py:1:0: E0401: Unable to import 'fastapi' (import-error) main.py:2:0: E0401: Unable to import 'fastapi.middleware.cors' (import-error) main.py:3:0: E0401: Unable to import 'fastapi.responses' (import-error) main.py:4:0: E0401: Unable to import 'fastapi.security' (import-error) main.py:5:0: E0401: Unable to import 'pydantic' (import-error) main.py:8:0: E0401: Unable to import 'psutil' (import-error) main.py:15:0: E0401: Unable to import 'passlib.context' (import-error) main.py:16:0: E0401: Unable to import 'jose' (import-error) main.py:18:0: E0401: Unable to import 'authlib.integrations.starlette_client' (import-error) main.py:19:0: E0401: Unable to import 'authlib.common.errors' (import-error) main.py:20:0: E0401: Unable to import 'httpx' (import-error) main.py:21:0: E0401: Unable to import 'dotenv' (import-error) main.py:74:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:85:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:91:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:95:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:106:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:106:41: W0621: Redefining name 'config' from outer scope (line 35) (redefined-outer-name) main.py:112:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:118:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:125:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:128:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:131:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:138:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:161:8: W0707: Consider explicitly re-raising using 'except JWTError as exc' and 'raise HTTPException(status_code=401, detail='Неверный токен') from exc' (raise-missing-from) main.py:163:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:182:21: W0621: Redefining name 'config' from outer scope (line 35) (redefined-outer-name) main.py:200:8: W0707: Consider explicitly re-raising using 'raise HTTPException(500, f'Ошибка инициализации OAuth: {str(e)}') from e' (raise-missing-from) main.py:236:8: W0707: Consider explicitly re-raising using 'raise HTTPException(400, f'OAuth ошибка: {str(e)}') from e' (raise-missing-from) main.py:239:8: W0707: Consider explicitly re-raising using 'raise HTTPException(500, f'Ошибка аутентификации: {str(e)}') from e' (raise-missing-from) main.py:259:4: C0415: Import outside toplevel (re) (import-outside-toplevel) main.py:270:4: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return) main.py:301:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:323:21: R1719: The if expression can be replaced with 'test' (simplifiable-if-expression) main.py:344:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:365:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:389:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:389:20: W0613: Unused argument 'user' (unused-argument) main.py:408:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:427:12: W0621: Redefining name 'config' from outer scope (line 35) (redefined-outer-name) main.py:436:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:460:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:692:12: W0621: Redefining name 'config' from outer scope (line 35) (redefined-outer-name) main.py:698:8: W0612: Unused variable 'username' (unused-variable) main.py:749:12: W0621: Redefining name 'config' from outer scope (line 35) (redefined-outer-name) main.py:802:12: W0621: Redefining name 'config' from outer scope (line 35) (redefined-outer-name) main.py:837:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:849:16: W0621: Redefining name 'config' from outer scope (line 35) (redefined-outer-name) main.py:865:11: W0718: Catching too general exception Exception (broad-exception-caught) main.py:870:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:881:4: W0621: Redefining name 'config' from outer scope (line 35) (redefined-outer-name) main.py:902:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:910:4: W0621: Redefining name 'config' from outer scope (line 35) (redefined-outer-name) main.py:915:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:915:49: W0621: Redefining name 'config' from outer scope (line 35) (redefined-outer-name) main.py:930:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:945:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:972:11: W0718: Catching too general exception Exception (broad-exception-caught) main.py:968:19: W0718: Catching too general exception Exception (broad-exception-caught) main.py:981:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:992:4: W0621: Redefining name 'config' from outer scope (line 35) (redefined-outer-name) main.py:1029:8: W0707: Consider explicitly re-raising using 'raise HTTPException(500, f'Ошибка запуска сервера: {str(e)}') from e' (raise-missing-from) main.py:1032:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1052:11: W0718: Catching too general exception Exception (broad-exception-caught) main.py:1057:8: W0702: No exception type(s) specified (bare-except) main.py:1067:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1082:8: R1705: Unnecessary "else" after "return", remove the "else" and de-indent the code inside it (no-else-return) main.py:1091:8: W0707: Consider explicitly re-raising using 'raise HTTPException(500, f'Ошибка отправки команды: {str(e)}') from e' (raise-missing-from) main.py:1094:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1103:4: W0702: No exception type(s) specified (bare-except) main.py:1144:11: W0718: Catching too general exception Exception (broad-exception-caught) main.py:1154:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1177:11: W0718: Catching too general exception Exception (broad-exception-caught) main.py:1179:8: W0107: Unnecessary pass statement (unnecessary-pass) main.py:1183:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1199:8: W0707: Consider explicitly re-raising using 'except Exception as exc' and 'raise HTTPException(404, 'Путь не найден') from exc' (raise-missing-from) main.py:1217:8: W0707: Consider explicitly re-raising using 'raise HTTPException(500, f'Ошибка чтения директории: {str(e)}') from e' (raise-missing-from) main.py:1222:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1235:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1256:8: W0707: Consider explicitly re-raising using 'raise HTTPException(500, f'Ошибка создания директории: {str(e)}') from e' (raise-missing-from) main.py:1265:8: W0707: Consider explicitly re-raising using 'raise HTTPException(500, f'Ошибка записи файла: {str(e)}') from e' (raise-missing-from) main.py:1316:8: W0707: Consider explicitly re-raising using 'raise HTTPException(500, f'Ошибка создания: {str(e)}') from e' (raise-missing-from) main.py:1319:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1337:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1352:8: W0707: Consider explicitly re-raising using 'except UnicodeDecodeError as exc' and 'raise HTTPException(400, 'Файл не является текстовым') from exc' (raise-missing-from) main.py:1355:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1370:8: W0707: Consider explicitly re-raising using 'raise HTTPException(400, f'Ошибка сохранения файла: {str(e)}') from e' (raise-missing-from) main.py:1373:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1439:8: W0621: Redefining name 'shutil' from outer scope (line 10) (redefined-outer-name) main.py:1439:8: W0404: Reimport 'shutil' (imported line 10) (reimported) main.py:1439:8: C0415: Import outside toplevel (shutil) (import-outside-toplevel) main.py:1446:8: W0707: Consider explicitly re-raising using 'raise HTTPException(500, f'Ошибка перемещения: {str(e)}') from e' (raise-missing-from) main.py:1449:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1449:0: E0102: function already defined line 1373 (function-redefined) main.py:1623:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1010:22: R1732: Consider using 'with' for resource-allocating operations (consider-using-with) main.py:1630:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1635:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1639:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1645:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1645:0: E0102: function already defined line 389 (function-redefined) main.py:1650:8: W0612: Unused variable 'username' (unused-variable) main.py:1658:0: C0115: Missing class docstring (missing-class-docstring) main.py:1658:0: R0903: Too few public methods (0/2) (too-few-public-methods) main.py:1662:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1675:52: W1309: Using an f-string that does not have any interpolated variables (f-string-without-interpolation) main.py:1720:0: C0115: Missing class docstring (missing-class-docstring) main.py:1720:0: R0903: Too few public methods (0/2) (too-few-public-methods) main.py:1724:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1755:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1780:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1780:0: E0102: function already defined line 436 (function-redefined) main.py:1803:0: C0115: Missing class docstring (missing-class-docstring) main.py:1803:0: R0903: Too few public methods (0/2) (too-few-public-methods) main.py:1807:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1833:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1854:0: C0115: Missing class docstring (missing-class-docstring) main.py:1854:0: R0903: Too few public methods (0/2) (too-few-public-methods) main.py:1858:0: C0116: Missing function or method docstring (missing-function-docstring) main.py:1858:0: E0102: function already defined line 516 (function-redefined) main.py:1873:4: E0401: Unable to import 'uvicorn' (import-error) main.py:6:0: C0411: standard import "asyncio" should be placed before third party imports "fastapi.FastAPI", "fastapi.middleware.cors.CORSMiddleware", "fastapi.responses.FileResponse", "fastapi.security.HTTPBearer", "pydantic.BaseModel" (wrong-import-order) main.py:7:0: C0411: standard import "subprocess" should be placed before third party imports "fastapi.FastAPI", "fastapi.middleware.cors.CORSMiddleware", "fastapi.responses.FileResponse", "fastapi.security.HTTPBearer", "pydantic.BaseModel" (wrong-import-order) main.py:9:0: C0411: standard import "os" should be placed before third party imports "fastapi.FastAPI", "fastapi.middleware.cors.CORSMiddleware", "fastapi.responses.FileResponse", "fastapi.security.HTTPBearer", "pydantic.BaseModel", "psutil" (wrong-import-order) main.py:10:0: C0411: standard import "shutil" should be placed before third party imports "fastapi.FastAPI", "fastapi.middleware.cors.CORSMiddleware", "fastapi.responses.FileResponse", "fastapi.security.HTTPBearer", "pydantic.BaseModel", "psutil" (wrong-import-order) main.py:11:0: C0411: standard import "sys" should be placed before third party imports "fastapi.FastAPI", "fastapi.middleware.cors.CORSMiddleware", "fastapi.responses.FileResponse", "fastapi.security.HTTPBearer", "pydantic.BaseModel", "psutil" (wrong-import-order) main.py:12:0: C0411: standard import "pathlib.Path" should be placed before third party imports "fastapi.FastAPI", "fastapi.middleware.cors.CORSMiddleware", "fastapi.responses.FileResponse", "fastapi.security.HTTPBearer", "pydantic.BaseModel", "psutil" (wrong-import-order) main.py:13:0: C0411: standard import "typing.Optional" should be placed before third party imports "fastapi.FastAPI", "fastapi.middleware.cors.CORSMiddleware", "fastapi.responses.FileResponse", "fastapi.security.HTTPBearer", "pydantic.BaseModel", "psutil" (wrong-import-order) main.py:14:0: C0411: standard import "json" should be placed before third party imports "fastapi.FastAPI", "fastapi.middleware.cors.CORSMiddleware", "fastapi.responses.FileResponse", "fastapi.security.HTTPBearer", "pydantic.BaseModel", "psutil" (wrong-import-order) main.py:17:0: C0411: standard import "datetime.datetime" should be placed before third party imports "fastapi.FastAPI", "fastapi.middleware.cors.CORSMiddleware", "fastapi.responses.FileResponse" (...) "psutil", "passlib.context.CryptContext", "jose.JWTError" (wrong-import-order) main.py:1:0: W0611: Unused status imported from fastapi (unused-import) main.py:13:0: W0611: Unused Optional imported from typing (unused-import) main.py:20:0: W0611: Unused import httpx (unused-import) main.py:22:0: W0611: Unused OIDC_PROVIDERS imported from oidc_config (unused-import) ************* Module auth auth.py:56:0: C0303: Trailing whitespace (trailing-whitespace) auth.py:62:0: C0303: Trailing whitespace (trailing-whitespace) auth.py:69:0: C0303: Trailing whitespace (trailing-whitespace) auth.py:76:0: C0303: Trailing whitespace (trailing-whitespace) auth.py:92:0: C0303: Trailing whitespace (trailing-whitespace) auth.py:1:0: C0114: Missing module docstring (missing-module-docstring) auth.py:3:0: E0401: Unable to import 'jose' (import-error) auth.py:4:0: E0401: Unable to import 'passlib.context' (import-error) auth.py:5:0: E0401: Unable to import 'fastapi' (import-error) auth.py:6:0: E0401: Unable to import 'fastapi.security' (import-error) auth.py:20:0: C0116: Missing function or method docstring (missing-function-docstring) auth.py:26:0: C0116: Missing function or method docstring (missing-function-docstring) auth.py:30:0: C0116: Missing function or method docstring (missing-function-docstring) auth.py:33:0: C0116: Missing function or method docstring (missing-function-docstring) auth.py:36:0: C0116: Missing function or method docstring (missing-function-docstring) auth.py:46:0: C0116: Missing function or method docstring (missing-function-docstring) auth.py:53:0: C0116: Missing function or method docstring (missing-function-docstring) auth.py:79:0: C0116: Missing function or method docstring (missing-function-docstring) auth.py:88:0: C0116: Missing function or method docstring (missing-function-docstring) auth.py:7:0: C0411: standard import "json" should be placed before third party imports "jose.JWTError", "passlib.context.CryptContext", "fastapi.Depends", "fastapi.security.HTTPBearer" (wrong-import-order) auth.py:8:0: C0411: standard import "pathlib.Path" should be placed before third party imports "jose.JWTError", "passlib.context.CryptContext", "fastapi.Depends", "fastapi.security.HTTPBearer" (wrong-import-order) auth.py:1:0: R0801: Similar lines in 2 files ==main:[1623:1647] ==user_management_endpoints:[28:54] users_file = Path("users.json") if not users_file.exists(): return {} with open(users_file, "r", encoding="utf-8") as f: return json.load(f) def save_users_dict(users): with open("users.json", "w", encoding="utf-8") as f: json.dump(users, f, indent=2, ensure_ascii=False) # Проверка прав def require_owner(current_user: dict): if current_user.get("role") != "owner": raise HTTPException(status_code=403, detail="Требуется роль владельца") def require_admin_or_owner(current_user: dict): if current_user.get("role") not in ["owner", "admin"]: raise HTTPException(status_code=403, detail="Требуется роль администратора или владельца") # 1. Получить список пользователей @app.get("/api/users") async def get_users(current_user: dict = Depends(get_current_user)): require_admin_or_owner(current_user) (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==main:[1811:1826] ==user_management_endpoints:[180:195] if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") if "resource_access" not in users[username]: users[username]["resource_access"] = {"servers": [], "tickets": [], "files": []} if access.server_name not in users[username]["resource_access"]["servers"]: users[username]["resource_access"]["servers"].append(access.server_name) # Также добавляем в старое поле servers для совместимости if "servers" not in users[username]: users[username]["servers"] = [] if access.server_name not in users[username]["servers"]: users[username]["servers"].append(access.server_name) (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==migrate_users:[72:80] ==user_management_endpoints:[96:104] "manage_users": True, "manage_roles": True, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": True, "view_all_resources": True } (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==migrate_users:[104:112] ==user_management_endpoints:[106:114] "manage_users": True, "manage_roles": False, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": False, "view_all_resources": True } (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==migrate_users:[115:123] ==user_management_endpoints:[116:124] "manage_users": False, "manage_roles": False, "manage_servers": False, "manage_tickets": True, "manage_files": False, "delete_users": False, "view_all_resources": False } (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==migrate_users:[126:134] ==user_management_endpoints:[126:134] "manage_users": False, "manage_roles": False, "manage_servers": False, "manage_tickets": False, "manage_files": False, "delete_users": False, "view_all_resources": False } (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==main:[1837:1848] ==user_management_endpoints:[210:221] if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") if "resource_access" in users[username] and "servers" in users[username]["resource_access"]: if server_name in users[username]["resource_access"]["servers"]: users[username]["resource_access"]["servers"].remove(server_name) # Также удаляем из старого поля servers if "servers" in users[username] and server_name in users[username]["servers"]: users[username]["servers"].remove(server_name) (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==main:[1648:1657] ==user_management_endpoints:[57:68] users_list = [] for username, user_data in users.items(): user_copy = user_data.copy() user_copy.pop("password", None) users_list.append(user_copy) return users_list # 2. Изменить роль пользователя (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==main:[1666:1674] ==user_management_endpoints:[72:81] if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") if username == current_user.get("username"): raise HTTPException(status_code=400, detail="Нельзя изменить свою роль") valid_roles = ["owner", "admin", "support", "user", "banned"] if role_data.role not in valid_roles: (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==main:[1759:1767] ==user_management_endpoints:[296:304] if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") if users[username].get("role") != "banned": raise HTTPException(status_code=400, detail="Пользователь не заблокирован") users[username]["role"] = "user" users[username]["permissions"] = { (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==auth:[20:30] ==main:[85:95] if USERS_FILE.exists(): with open(USERS_FILE, 'r', encoding='utf-8') as f: return json.load(f) return {} def save_users(users: dict): with open(USERS_FILE, 'w', encoding='utf-8') as f: json.dump(users, f, indent=2, ensure_ascii=False) def load_server_config(server_name: str) -> dict: (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==migrate_users:[105:110] ==user_management_endpoints:[137:142] "manage_roles": False, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": False, (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==migrate_users:[138:143] ==user_management_endpoints:[107:112] "manage_roles": False, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": False, (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==migrate_users:[137:145] ==user_management_endpoints:[136:145] "manage_users": False, "manage_roles": False, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": False, "view_all_resources": False } (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==main:[1784:1792] ==user_management_endpoints:[236:243] if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") if username == current_user.get("username"): raise HTTPException(status_code=400, detail="Нельзя удалить самого себя") # Проверяем, что не удаляем последнего владельца if users[username].get("role") == "owner": (duplicate-code) auth.py:1:0: R0801: Similar lines in 2 files ==main:[1728:1736] ==user_management_endpoints:[260:267] if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") if username == current_user.get("username"): raise HTTPException(status_code=400, detail="Нельзя заблокировать самого себя") if users[username].get("role") == "owner": (duplicate-code) ----------------------------------- Your code has been rated at 5.22/10 + echo "Checking code formatting with black..." Checking code formatting with black... + black --check --diff . --- /drone/src/backend/models.py 2026-01-15 13:56:18.978682+00:00 +++ /drone/src/backend/models.py 2026-01-15 13:56:47.031210+00:00 @@ -1,23 +1,28 @@ from pydantic import BaseModel from typing import Optional, List + class UserRegister(BaseModel): username: str password: str + class UserLogin(BaseModel): username: str password: str + class Token(BaseModel): access_token: str token_type: str username: str role: str + class ServerAccess(BaseModel): username: str server_name: str + class ServerAccessList(BaseModel): users: List[dict] would reformat /drone/src/backend/models.py --- /drone/src/backend/oidc_config.py 2026-01-15 13:56:18.978682+00:00 +++ /drone/src/backend/oidc_config.py 2026-01-15 13:56:47.059348+00:00 @@ -1,31 +1,35 @@ """ Конфигурация OpenID Connect провайдеров """ + import os from typing import Dict, Any # Конфигурация провайдеров OpenID Connect OIDC_PROVIDERS = { "zitadel": { "name": "ZITADEL", "client_id": os.getenv("ZITADEL_CLIENT_ID", ""), "client_secret": os.getenv("ZITADEL_CLIENT_SECRET", ""), - "server_metadata_url": os.getenv("ZITADEL_ISSUER", "") + "/.well-known/openid-configuration", + "server_metadata_url": os.getenv("ZITADEL_ISSUER", "") + + "/.well-known/openid-configuration", "issuer": os.getenv("ZITADEL_ISSUER", ""), "scopes": ["openid", "email", "profile"], "icon": "🔐", - "color": "bg-purple-600 hover:bg-purple-700" + "color": "bg-purple-600 hover:bg-purple-700", } } + def get_enabled_providers() -> Dict[str, Dict[str, Any]]: """Получить список включённых провайдеров (с настроенными client_id)""" enabled = {} for provider_id, config in OIDC_PROVIDERS.items(): if config.get("client_id") and config.get("issuer"): enabled[provider_id] = config return enabled + def get_redirect_uri(provider_id: str, base_url: str = "http://localhost:8000") -> str: """Получить redirect URI для провайдера""" - return f"{base_url}/api/auth/oidc/{provider_id}/callback" \ No newline at end of file + return f"{base_url}/api/auth/oidc/{provider_id}/callback" would reformat /drone/src/backend/oidc_config.py --- /drone/src/backend/auth.py 2026-01-15 13:56:18.974682+00:00 +++ /drone/src/backend/auth.py 2026-01-15 13:56:47.113090+00:00 @@ -15,25 +15,30 @@ security = HTTPBearer() USERS_FILE = Path("data/users.json") USERS_FILE.parent.mkdir(exist_ok=True) + def load_users(): if USERS_FILE.exists(): - with open(USERS_FILE, 'r', encoding='utf-8') as f: + with open(USERS_FILE, "r", encoding="utf-8") as f: return json.load(f) return {} + def save_users(users): - with open(USERS_FILE, 'w', encoding='utf-8') as f: + with open(USERS_FILE, "w", encoding="utf-8") as f: json.dump(users, f, indent=2, ensure_ascii=False) + def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) + def get_password_hash(password): return pwd_context.hash(password) + def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta @@ -41,72 +46,79 @@ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt + def decode_token(token: str): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) return payload except JWTError: return None -async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), +): token = credentials.credentials payload = decode_token(token) - + if payload is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Неверный токен авторизации" + detail="Неверный токен авторизации", ) - + username: str = payload.get("sub") if username is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Неверный токен авторизации" + detail="Неверный токен авторизации", ) - + users = load_users() if username not in users: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Пользователь не найден" + status_code=status.HTTP_401_UNAUTHORIZED, detail="Пользователь не найден" ) - + return {"username": username, "role": users[username].get("role", "user")} + def authenticate_user(username: str, password: str): users = load_users() if username not in users: return False user = users[username] if not verify_password(password, user["password"]): return False return user + def create_user(username: str, password: str, role: str = "user"): users = load_users() if username in users: return False - + users[username] = { "password": get_password_hash(password), "role": role, "created_at": datetime.utcnow().isoformat(), - "servers": [] # Список серверов к которым есть доступ + "servers": [], # Список серверов к которым есть доступ } save_users(users) return True + def get_user_servers(username: str): """Получить список серверов пользователя""" users = load_users() if username not in users: return [] return users[username].get("servers", []) + def add_server_to_user(username: str, server_name: str): """Добавить сервер пользователю""" users = load_users() if username not in users: @@ -116,31 +128,31 @@ if server_name not in users[username]["servers"]: users[username]["servers"].append(server_name) save_users(users) return True + def remove_server_from_user(username: str, server_name: str): """Удалить сервер у пользователя""" users = load_users() if username not in users: return False if "servers" in users[username] and server_name in users[username]["servers"]: users[username]["servers"].remove(server_name) save_users(users) return True + def get_server_users(server_name: str): """Получить список пользователей с доступом к серверу""" users = load_users() result = [] for username, user_data in users.items(): if server_name in user_data.get("servers", []): - result.append({ - "username": username, - "role": user_data.get("role", "user") - }) + result.append({"username": username, "role": user_data.get("role", "user")}) return result + def has_server_access(username: str, server_name: str): """Проверить есть ли доступ к серверу""" users = load_users() if username not in users: would reformat /drone/src/backend/auth.py --- /drone/src/backend/migrate_users.py 2026-01-15 13:56:18.978682+00:00 +++ /drone/src/backend/migrate_users.py 2026-01-15 13:56:47.227169+00:00 @@ -6,21 +6,24 @@ import json from pathlib import Path from datetime import datetime + def migrate_users(): """Миграция пользователей на новую систему прав""" - + users_file = Path("users.json") - + # Проверка существования файла if not users_file.exists(): print("❌ Файл users.json не найден") - print("ℹ️ Создайте файл users.json или запустите панель для автоматического создания") - return False - + print( + "ℹ️ Создайте файл users.json или запустите панель для автоматического создания" + ) + return False + # Создание backup backup_file = Path(f"users_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json") try: with open(users_file, "r", encoding="utf-8") as f: backup_data = f.read() @@ -28,22 +31,22 @@ f.write(backup_data) print(f"✅ Backup создан: {backup_file}") except Exception as e: print(f"❌ Ошибка создания backup: {e}") return False - + # Загрузка пользователей try: with open(users_file, "r", encoding="utf-8") as f: users_data = json.load(f) except json.JSONDecodeError: print("❌ Ошибка чтения users.json - неверный формат JSON") return False except Exception as e: print(f"❌ Ошибка чтения файла: {e}") return False - + # Проверка формата (объект или список) if isinstance(users_data, dict): # Формат: {"username": {...}} users_list = list(users_data.values()) is_dict_format = True @@ -54,18 +57,18 @@ is_dict_format = False print("ℹ️ Обнаружен формат: список") else: print("❌ Неизвестный формат users.json") return False - + if not users_list: print("ℹ️ Нет пользователей для миграции") return True - + print(f"\n📊 Найдено пользователей: {len(users_list)}") print("=" * 50) - + # Миграция первого пользователя (владелец) if users_list: first_user = users_list[0] print(f"\n👑 Назначение владельца: {first_user.get('username', 'Unknown')}") first_user["role"] = "owner" @@ -74,88 +77,93 @@ "manage_roles": True, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": True, - "view_all_resources": True + "view_all_resources": True, } if "resource_access" not in first_user: first_user["resource_access"] = { "servers": first_user.get("servers", []), "tickets": [], - "files": [] + "files": [], } - + # Миграция остальных пользователей for i, user in enumerate(users_list[1:], start=2): username = user.get("username", f"User{i}") current_role = user.get("role", "user") - + print(f"\n👤 Пользователь {i}: {username}") print(f" Текущая роль: {current_role}") - + # Установка роли по умолчанию - if "role" not in user or user["role"] not in ["admin", "support", "user", "banned"]: + if "role" not in user or user["role"] not in [ + "admin", + "support", + "user", + "banned", + ]: user["role"] = "user" print(f" ➜ Установлена роль: user") - + # Добавление прав if "permissions" not in user: if user["role"] == "admin": user["permissions"] = { "manage_users": True, "manage_roles": False, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": False, - "view_all_resources": True + "view_all_resources": True, } print(" ➜ Добавлены права администратора") elif user["role"] == "support": user["permissions"] = { "manage_users": False, "manage_roles": False, "manage_servers": False, "manage_tickets": True, "manage_files": False, "delete_users": False, - "view_all_resources": False + "view_all_resources": False, } print(" ➜ Добавлены права поддержки") elif user["role"] == "banned": user["permissions"] = { "manage_users": False, "manage_roles": False, "manage_servers": False, "manage_tickets": False, "manage_files": False, "delete_users": False, - "view_all_resources": False + "view_all_resources": False, } print(" ➜ Пользователь заблокирован") else: # user user["permissions"] = { "manage_users": False, "manage_roles": False, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": False, - "view_all_resources": False + "view_all_resources": False, } print(" ➜ Добавлены права пользователя") - + # Добавление доступа к ресурсам if "resource_access" not in user: user["resource_access"] = { "servers": user.get("servers", []), "tickets": [], - "files": [] + "files": [], } print(" ➜ Добавлен доступ к ресурсам") - + would reformat /drone/src/backend/migrate_users.py # Сохранение в правильном формате try: if is_dict_format: # Сохраняем обратно как объект users_dict = {user["username"]: user for user in users_list} @@ -163,11 +171,11 @@ json.dump(users_dict, f, indent=2, ensure_ascii=False) else: # Сохраняем как список with open(users_file, "w", encoding="utf-8") as f: json.dump(users_list, f, indent=2, ensure_ascii=False) - + print("\n" + "=" * 50) print("✅ Миграция успешно завершена!") print(f"✅ Обновлено пользователей: {len(users_list)}") print(f"👑 Владелец: {users_list[0]['username']}") print(f"📁 Backup: {backup_file}") @@ -175,62 +183,64 @@ except Exception as e: print(f"\n❌ Ошибка сохранения: {e}") print(f"ℹ️ Восстановите из backup: {backup_file}") return False + def show_users(): """Показать список пользователей после миграции""" users_file = Path("users.json") - + if not users_file.exists(): print("❌ Файл users.json не найден") return - + try: with open(users_file, "r", encoding="utf-8") as f: users_data = json.load(f) except Exception as e: print(f"❌ Ошибка чтения файла: {e}") return - + # Преобразуем в список если это объект if isinstance(users_data, dict): users_list = list(users_data.values()) else: users_list = users_data - + print("\n" + "=" * 50) print("📋 СПИСОК ПОЛЬЗОВАТЕЛЕЙ") print("=" * 50) - + for i, user in enumerate(users_list, start=1): print(f"\n{i}. {user.get('username', 'Unknown')}") print(f" Роль: {user.get('role', 'unknown')}") print(f" Права:") - for perm, value in user.get('permissions', {}).items(): + for perm, value in user.get("permissions", {}).items(): status = "✅" if value else "❌" print(f" {status} {perm}") - + # Показать доступ к ресурсам - resource_access = user.get('resource_access', {}) + resource_access = user.get("resource_access", {}) if resource_access: - servers = resource_access.get('servers', []) + servers = resource_access.get("servers", []) if servers: print(f" Серверы: {', '.join(servers)}") + if __name__ == "__main__": print("=" * 50) print("MC Panel - Миграция пользователей v1.1.0") print("=" * 50) - + # Запуск миграции success = migrate_users() - + if success: # Показать результат show_users() - + print("\n" + "=" * 50) print("📝 СЛЕДУЮЩИЕ ШАГИ:") print("=" * 50) print("1. Перезапустите панель") print("2. Войдите как владелец") --- /drone/src/backend/user_management_endpoints.py 2026-01-15 13:56:18.978682+00:00 +++ /drone/src/backend/user_management_endpoints.py 2026-01-15 13:56:47.279409+00:00 @@ -9,312 +9,335 @@ import json from pathlib import Path router = APIRouter() + # Модели данных class RoleChange(BaseModel): role: str + class PermissionsUpdate(BaseModel): permissions: dict + class ServerAccess(BaseModel): server_name: str + class BanRequest(BaseModel): reason: str = "Заблокирован администратором" + # Загрузка пользователей def load_users(): users_file = Path("users.json") if not users_file.exists(): return {} - + with open(users_file, "r", encoding="utf-8") as f: return json.load(f) + # Сохранение пользователей def save_users(users): with open("users.json", "w", encoding="utf-8") as f: json.dump(users, f, indent=2, ensure_ascii=False) + # Проверка прав def require_owner(current_user: dict): if current_user.get("role") != "owner": raise HTTPException(status_code=403, detail="Требуется роль владельца") + def require_admin_or_owner(current_user: dict): if current_user.get("role") not in ["owner", "admin"]: - raise HTTPException(status_code=403, detail="Требуется роль администратора или владельца") + raise HTTPException( + status_code=403, detail="Требуется роль администратора или владельца" + ) + # 1. Получить список пользователей @router.get("/api/users") async def get_users(current_user: dict = Depends()): require_admin_or_owner(current_user) - - users = load_users() - + + users = load_users() + # Возвращаем список пользователей (без паролей) users_list = [] for username, user_data in users.items(): user_copy = user_data.copy() user_copy.pop("password", None) users_list.append(user_copy) - + return users_list + # 2. Изменить роль пользователя @router.put("/api/users/{username}/role") -async def change_user_role(username: str, role_data: RoleChange, current_user: dict = Depends()): +async def change_user_role( + username: str, role_data: RoleChange, current_user: dict = Depends() +): require_owner(current_user) - - users = load_users() - - if username not in users: - raise HTTPException(status_code=404, detail="Пользователь не найден") - + + users = load_users() + + if username not in users: + raise HTTPException(status_code=404, detail="Пользователь не найден") + if username == current_user.get("username"): raise HTTPException(status_code=400, detail="Нельзя изменить свою роль") - + # Проверка валидности роли valid_roles = ["owner", "admin", "support", "user", "banned"] if role_data.role not in valid_roles: - raise HTTPException(status_code=400, detail=f"Неверная роль. Доступные: {', '.join(valid_roles)}") - + raise HTTPException( + status_code=400, + detail=f"Неверная роль. Доступные: {', '.join(valid_roles)}", + ) + # Если назначается новый owner, текущий owner становится admin if role_data.role == "owner": for user in users.values(): if user.get("role") == "owner": user["role"] = "admin" - + # Изменяем роль old_role = users[username].get("role", "user") users[username]["role"] = role_data.role - + # Обновляем права в зависимости от роли if role_data.role == "owner": users[username]["permissions"] = { would reformat /drone/src/backend/user_management_endpoints.py "manage_users": True, "manage_roles": True, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": True, - "view_all_resources": True + "view_all_resources": True, } elif role_data.role == "admin": users[username]["permissions"] = { "manage_users": True, "manage_roles": False, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": False, - "view_all_resources": True + "view_all_resources": True, } elif role_data.role == "support": users[username]["permissions"] = { "manage_users": False, "manage_roles": False, "manage_servers": False, "manage_tickets": True, "manage_files": False, "delete_users": False, - "view_all_resources": False + "view_all_resources": False, } elif role_data.role == "banned": users[username]["permissions"] = { "manage_users": False, "manage_roles": False, "manage_servers": False, "manage_tickets": False, "manage_files": False, "delete_users": False, - "view_all_resources": False + "view_all_resources": False, } else: # user users[username]["permissions"] = { "manage_users": False, "manage_roles": False, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": False, - "view_all_resources": False - } - - save_users(users) - + "view_all_resources": False, + } + + save_users(users) + return { "message": f"Роль пользователя {username} изменена с {old_role} на {role_data.role}", - "user": { - "username": username, - "role": role_data.role - } - } + "user": {"username": username, "role": role_data.role}, + } + # 3. Изменить права пользователя @router.put("/api/users/{username}/permissions") -async def update_user_permissions(username: str, perms: PermissionsUpdate, current_user: dict = Depends()): +async def update_user_permissions( + username: str, perms: PermissionsUpdate, current_user: dict = Depends() +): require_owner(current_user) - - users = load_users() - - if username not in users: - raise HTTPException(status_code=404, detail="Пользователь не найден") - + + users = load_users() + + if username not in users: + raise HTTPException(status_code=404, detail="Пользователь не найден") + users[username]["permissions"] = perms.permissions save_users(users) - + return { "message": f"Права пользователя {username} обновлены", - "permissions": perms.permissions - } + "permissions": perms.permissions, + } + # 4. Выдать доступ к серверу @router.post("/api/users/{username}/access/servers") -async def grant_server_access(username: str, access: ServerAccess, current_user: dict = Depends()): - require_admin_or_owner(current_user) - - users = load_users() - - if username not in users: - raise HTTPException(status_code=404, detail="Пользователь не найден") - +async def grant_server_access( + username: str, access: ServerAccess, current_user: dict = Depends() +): + require_admin_or_owner(current_user) + + users = load_users() + + if username not in users: + raise HTTPException(status_code=404, detail="Пользователь не найден") + if "resource_access" not in users[username]: users[username]["resource_access"] = {"servers": [], "tickets": [], "files": []} - + if access.server_name not in users[username]["resource_access"]["servers"]: users[username]["resource_access"]["servers"].append(access.server_name) - + # Также добавляем в старое поле servers для совместимости if "servers" not in users[username]: users[username]["servers"] = [] if access.server_name not in users[username]["servers"]: users[username]["servers"].append(access.server_name) - - save_users(users) - + + save_users(users) + return { "message": f"Доступ к серверу {access.server_name} выдан пользователю {username}", "server": access.server_name, - "user": username - } + "user": username, + } + # 5. Забрать доступ к серверу @router.delete("/api/users/{username}/access/servers/{server_name}") -async def revoke_server_access(username: str, server_name: str, current_user: dict = Depends()): - require_admin_or_owner(current_user) - - users = load_users() - - if username not in users: - raise HTTPException(status_code=404, detail="Пользователь не найден") - - if "resource_access" in users[username] and "servers" in users[username]["resource_access"]: +async def revoke_server_access( + username: str, server_name: str, current_user: dict = Depends() +): + require_admin_or_owner(current_user) + + users = load_users() + + if username not in users: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + if ( + "resource_access" in users[username] + and "servers" in users[username]["resource_access"] + ): if server_name in users[username]["resource_access"]["servers"]: users[username]["resource_access"]["servers"].remove(server_name) - + # Также удаляем из старого поля servers if "servers" in users[username] and server_name in users[username]["servers"]: users[username]["servers"].remove(server_name) - - save_users(users) - + + save_users(users) + return { "message": f"Доступ к серверу {server_name} отозван у пользователя {username}", "server": server_name, - "user": username - } + "user": username, + } + # 6. Удалить пользователя @router.delete("/api/users/{username}") async def delete_user(username: str, current_user: dict = Depends()): require_owner(current_user) - - users = load_users() - - if username not in users: - raise HTTPException(status_code=404, detail="Пользователь не найден") - + + users = load_users() + + if username not in users: + raise HTTPException(status_code=404, detail="Пользователь не найден") + if username == current_user.get("username"): raise HTTPException(status_code=400, detail="Нельзя удалить самого себя") - + if users[username].get("role") == "owner": raise HTTPException(status_code=400, detail="Нельзя удалить владельца") - + del users[username] save_users(users) - - return { - "message": f"Пользователь {username} удалён", - "username": username - } + + return {"message": f"Пользователь {username} удалён", "username": username} + # 7. Заблокировать пользователя @router.post("/api/users/{username}/ban") async def ban_user(username: str, ban_data: BanRequest, current_user: dict = Depends()): require_admin_or_owner(current_user) - - users = load_users() - - if username not in users: - raise HTTPException(status_code=404, detail="Пользователь не найден") - + + users = load_users() + + if username not in users: + raise HTTPException(status_code=404, detail="Пользователь не найден") + if username == current_user.get("username"): raise HTTPException(status_code=400, detail="Нельзя заблокировать самого себя") - + if users[username].get("role") == "owner": raise HTTPException(status_code=400, detail="Нельзя заблокировать владельца") - + users[username]["role"] = "banned" users[username]["permissions"] = { "manage_users": False, "manage_roles": False, "manage_servers": False, "manage_tickets": False, "manage_files": False, "delete_users": False, - "view_all_resources": False + "view_all_resources": False, } users[username]["ban_reason"] = ban_data.reason - - save_users(users) - + + save_users(users) + return { "message": f"Пользователь {username} заблокирован", "username": username, - "reason": ban_data.reason - } + "reason": ban_data.reason, + } + # 8. Разблокировать пользователя @router.post("/api/users/{username}/unban") async def unban_user(username: str, current_user: dict = Depends()): require_admin_or_owner(current_user) - - users = load_users() - - if username not in users: - raise HTTPException(status_code=404, detail="Пользователь не найден") - + + users = load_users() + + if username not in users: + raise HTTPException(status_code=404, detail="Пользователь не найден") + if users[username].get("role") != "banned": raise HTTPException(status_code=400, detail="Пользователь не заблокирован") - + users[username]["role"] = "user" users[username]["permissions"] = { "manage_users": False, "manage_roles": False, "manage_servers": True, "manage_tickets": True, "manage_files": True, "delete_users": False, - "view_all_resources": False + "view_all_resources": False, } users[username].pop("ban_reason", None) - - save_users(users) - - return { - "message": f"Пользователь {username} разблокирован", - "username": username - } + + save_users(users) + + return {"message": f"Пользователь {username} разблокирован", "username": username} --- /drone/src/backend/main.py 2026-01-15 13:56:18.978682+00:00 +++ /drone/src/backend/main.py 2026-01-15 13:56:48.258411+00:00 @@ -1,6 +1,15 @@ -from fastapi import FastAPI, WebSocket, UploadFile, File, HTTPException, Depends, status, Request +from fastapi import ( + FastAPI, + WebSocket, + UploadFile, + File, + HTTPException, + Depends, + status, + Request, +) from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, RedirectResponse from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from pydantic import BaseModel import asyncio @@ -36,11 +45,11 @@ oauth.register( name="zitadel", client_id=config["client_id"], client_secret=config["client_secret"], server_metadata_url=config["server_metadata_url"], - client_kwargs={"scope": " ".join(config["scopes"])} + client_kwargs={"scope": " ".join(config["scopes"])}, ) print(f"✓ ZITADEL провайдер зарегистрирован: {config['issuer']}") else: print("⚠ ZITADEL провайдер не настроен. Проверьте .env файл.") @@ -66,101 +75,114 @@ TICKETS_FILE = Path("tickets.json") server_processes: dict[str, subprocess.Popen] = {} server_logs: dict[str, list[str]] = {} -IS_WINDOWS = sys.platform == 'win32' +IS_WINDOWS = sys.platform == "win32" + # Инициализация файла пользователей def init_users(): if not USERS_FILE.exists(): admin_user = { "username": "Root", "password": pwd_context.hash("Admin"), "role": "admin", - "servers": [] + "servers": [], } save_users({"Sofa12345": admin_user}) print("Создан пользователь по умолчанию: none / none") + def load_users() -> dict: if USERS_FILE.exists(): - with open(USERS_FILE, 'r', encoding='utf-8') as f: + with open(USERS_FILE, "r", encoding="utf-8") as f: return json.load(f) return {} + def save_users(users: dict): - with open(USERS_FILE, 'w', encoding='utf-8') as f: + with open(USERS_FILE, "w", encoding="utf-8") as f: json.dump(users, f, indent=2, ensure_ascii=False) + def load_server_config(server_name: str) -> dict: config_path = SERVERS_DIR / server_name / "panel_config.json" if config_path.exists(): - with open(config_path, 'r', encoding='utf-8') as f: + with open(config_path, "r", encoding="utf-8") as f: return json.load(f) return { "name": server_name, "displayName": server_name, - "startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui" - } + "startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui", + } + def save_server_config(server_name: str, config: dict): config_path = SERVERS_DIR / server_name / "panel_config.json" - with open(config_path, 'w', encoding='utf-8') as f: + with open(config_path, "w", encoding="utf-8") as f: json.dump(config, f, indent=2, ensure_ascii=False) + # Функции для работы с тикетами def load_tickets() -> dict: if TICKETS_FILE.exists(): - with open(TICKETS_FILE, 'r', encoding='utf-8') as f: + with open(TICKETS_FILE, "r", encoding="utf-8") as f: return json.load(f) return {} + def save_tickets(tickets: dict): - with open(TICKETS_FILE, 'w', encoding='utf-8') as f: + with open(TICKETS_FILE, "w", encoding="utf-8") as f: json.dump(tickets, f, indent=2, ensure_ascii=False) + init_users() + # Функции аутентификации def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) + def get_password_hash(password): return pwd_context.hash(password) + def create_access_token(data: dict): to_encode = data.copy() expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt would reformat /drone/src/backend/main.py + def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): if not credentials: raise HTTPException(status_code=401, detail="Требуется авторизация") - + token = credentials.credentials try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise HTTPException(status_code=401, detail="Неверный токен") - + users = load_users() if username not in users: raise HTTPException(status_code=401, detail="Пользователь не найден") - + user = users[username] - + # Проверка на бан if user.get("role") == "banned": raise HTTPException(status_code=403, detail="Ваш аккаунт заблокирован") - + return user except JWTError: raise HTTPException(status_code=401, detail="Неверный токен") + def check_server_access(user: dict, server_name: str): # Владелец имеет доступ ко всем серверам if user["role"] == "owner": return True @@ -170,105 +192,117 @@ # Проверяем права на серверы if not user.get("permissions", {}).get("servers", True): return False return server_name in user.get("servers", []) + # API для аутентификации + # OpenID Connect endpoints @app.get("/api/auth/oidc/providers") async def get_oidc_providers(): """Получить список доступных OpenID Connect провайдеров""" providers = {} for provider_id, config in get_enabled_providers().items(): providers[provider_id] = { "name": config["name"], "icon": config["icon"], - "color": config["color"] + "color": config["color"], } return providers + @app.get("/api/auth/oidc/{provider}/login") async def oidc_login(provider: str, request: Request): """Начать процесс аутентификации через OpenID Connect""" if provider not in get_enabled_providers(): raise HTTPException(404, f"Провайдер {provider} не найден или не настроен") - + try: - redirect_uri = get_redirect_uri(provider, os.getenv("BASE_URL", "http://localhost:8000")) - return await oauth.create_client(provider).authorize_redirect(request, redirect_uri) + redirect_uri = get_redirect_uri( + provider, os.getenv("BASE_URL", "http://localhost:8000") + ) + return await oauth.create_client(provider).authorize_redirect( + request, redirect_uri + ) except Exception as e: raise HTTPException(500, f"Ошибка инициализации OAuth: {str(e)}") + @app.get("/api/auth/oidc/{provider}/callback") async def oidc_callback(provider: str, request: Request): """Обработка callback от OpenID Connect провайдера""" if provider not in get_enabled_providers(): raise HTTPException(404, f"Провайдер {provider} не найден или не настроен") - + try: client = oauth.create_client(provider) - + # Получаем токен от провайдера token = await client.authorize_access_token(request) - + # Получаем данные пользователя user_data = token.get("userinfo") if not user_data: # Если userinfo нет в токене, парсим id_token user_data = token.get("id_token") if not user_data: raise HTTPException(400, "Не удалось получить данные пользователя") - + # Создаём или обновляем пользователя username = create_or_update_oidc_user(user_data, provider) - + # Создаём JWT токен для нашей системы users = load_users() user = users[username] access_token = create_access_token({"sub": username, "role": user["role"]}) - + # Перенаправляем на фронтенд с токеном frontend_url = os.getenv("FRONTEND_URL", "http://localhost:3000") - return RedirectResponse(f"{frontend_url}/?token={access_token}&username={username}") - + return RedirectResponse( + f"{frontend_url}/?token={access_token}&username={username}" + ) + except AuthlibBaseError as e: print(f"OAuth ошибка для {provider}: {str(e)}") raise HTTPException(400, f"OAuth ошибка: {str(e)}") except Exception as e: print(f"Ошибка аутентификации для {provider}: {str(e)}") raise HTTPException(500, f"Ошибка аутентификации: {str(e)}") + def create_or_update_oidc_user(user_data: dict, provider: str) -> str: """Создать или обновить пользователя из OpenID Connect данных""" users = load_users() - + # Генерируем уникальное имя пользователя email = user_data.get("email", "") name = user_data.get("name", "") sub = user_data.get("sub", "") - + # Пытаемся использовать email как username if email: base_username = email.split("@")[0] elif name: base_username = name.replace(" ", "_").lower() else: base_username = f"{provider}_user" - + # Убираем недопустимые символы import re - base_username = re.sub(r'[^a-zA-Z0-9_-]', '', base_username) - + + base_username = re.sub(r"[^a-zA-Z0-9_-]", "", base_username) + # Ищем существующего пользователя по OIDC ID oidc_id = f"{provider}:{sub}" existing_user = None for username, user_info in users.items(): if user_info.get("oidc_id") == oidc_id: existing_user = username break - + if existing_user: # Обновляем существующего пользователя users[existing_user]["email"] = email users[existing_user]["name"] = name users[existing_user]["picture"] = user_data.get("picture") @@ -279,112 +313,115 @@ username = base_username counter = 1 while username in users: username = f"{base_username}_{counter}" counter += 1 - + users[username] = { "username": username, "password": "", # Пустой пароль для OIDC пользователей "role": "user", "servers": [], "oidc_id": oidc_id, "email": email, "name": name, "picture": user_data.get("picture"), "provider": provider, - "created_at": datetime.utcnow().isoformat() + "created_at": datetime.utcnow().isoformat(), } save_users(users) return username + @app.post("/api/auth/register") async def register(data: dict): users = load_users() username = data.get("username", "").strip() password = data.get("password", "").strip() - + if not username or not password: raise HTTPException(400, "Имя пользователя и пароль обязательны") - + if username in users: raise HTTPException(400, "Пользователь уже существует") - + # Первый пользователь становится владельцем role = "owner" if len(users) == 0 else "user" - + users[username] = { "username": username, "password": get_password_hash(password), "role": role, "servers": [], - "permissions": { - "servers": True, - "tickets": True, - "users": True if role == "owner" else False, - "files": True - } if role == "owner" else { - "servers": True, - "tickets": True, - "users": False, - "files": True - } - } - + "permissions": ( + { + "servers": True, + "tickets": True, + "users": True if role == "owner" else False, + "files": True, + } + if role == "owner" + else {"servers": True, "tickets": True, "users": False, "files": True} + ), + } + save_users(users) - + access_token = create_access_token(data={"sub": username}) return { "access_token": access_token, "token_type": "bearer", "username": username, - "role": role - } + "role": role, + } + @app.post("/api/auth/login") async def login(data: dict): users = load_users() username = data.get("username", "").strip() password = data.get("password", "").strip() - + if username not in users: raise HTTPException(401, "Неверное имя пользователя или пароль") - + user = users[username] if not verify_password(password, user["password"]): raise HTTPException(401, "Неверное имя пользователя или пароль") - + access_token = create_access_token(data={"sub": username}) return { "access_token": access_token, "token_type": "bearer", "username": username, - "role": user["role"] - } + "role": user["role"], + } + @app.get("/api/auth/me") async def get_me(user: dict = Depends(get_current_user)): users = load_users() user_data = users.get(user["username"], {}) - + # Если у пользователя нет прав, создаем дефолтные if "permissions" not in user_data: user_data["permissions"] = { "servers": True, "tickets": True, "users": user_data["role"] in ["owner", "admin"], - "files": True + "files": True, } users[user["username"]] = user_data save_users(users) - + return { "username": user["username"], "role": user["role"], "servers": user.get("servers", []), - "permissions": user_data.get("permissions", {}) - } + "permissions": user_data.get("permissions", {}), + } + # API для управления пользователями @app.get("/api/users") async def get_users(user: dict = Depends(get_current_user)): # Владелец, админы и тех. поддержка видят всех пользователей @@ -392,191 +429,207 @@ return [ { "username": u["username"], "role": u["role"], "servers": u.get("servers", []), - "permissions": u.get("permissions", { - "servers": True, - "tickets": True, - "users": u["role"] in ["owner", "admin"], - "files": True - }) + "permissions": u.get( + "permissions", + { + "servers": True, + "tickets": True, + "users": u["role"] in ["owner", "admin"], + "files": True, + }, + ), } for u in users.values() ] + @app.put("/api/users/{username}/servers") -async def update_user_servers(username: str, data: dict, user: dict = Depends(get_current_user)): +async def update_user_servers( + username: str, data: dict, user: dict = Depends(get_current_user) +): users = load_users() if username not in users: raise HTTPException(404, "Пользователь не найден") - + # Админы могут управлять доступом к любым серверам if user["role"] == "admin": users[username]["servers"] = data.get("servers", []) save_users(users) return {"message": "Доступ обновлен"} - + # Обычные пользователи могут управлять доступом только к своим серверам requested_servers = data.get("servers", []) current_servers = users[username].get("servers", []) - + # Проверяем, что пользователь пытается изменить доступ только к своим серверам for server_name in requested_servers: if server_name not in current_servers: # Проверяем, является ли текущий пользователь владельцем этого сервера config = load_server_config(server_name) if config.get("owner") != user["username"]: - raise HTTPException(403, f"Вы не можете выдать доступ к серверу {server_name}") - + raise HTTPException( + 403, f"Вы не можете выдать доступ к серверу {server_name}" + ) + users[username]["servers"] = requested_servers save_users(users) return {"message": "Доступ обновлен"} + @app.delete("/api/users/{username}") async def delete_user(username: str, user: dict = Depends(get_current_user)): # Только владелец может удалять пользователей if user["role"] != "owner": raise HTTPException(403, "Только владелец может удалять пользователей") - + if username == user["username"]: raise HTTPException(400, "Нельзя удалить самого себя") - + users = load_users() if username not in users: raise HTTPException(404, "Пользователь не найден") - + # Проверяем, что не удаляем последнего владельца if users[username]["role"] == "owner": owners_count = sum(1 for u in users.values() if u.get("role") == "owner") if owners_count <= 1: raise HTTPException(400, "Нельзя удалить последнего владельца") - + del users[username] save_users(users) - + return {"message": "Пользователь удален"} + @app.put("/api/users/{username}/role") -async def update_user_role(username: str, data: dict, user: dict = Depends(get_current_user)): +async def update_user_role( + username: str, data: dict, user: dict = Depends(get_current_user) +): # Только владелец может изменять роли if user["role"] != "owner": raise HTTPException(403, "Только владелец может изменять роли") - + if username == user["username"]: raise HTTPException(400, "Нельзя изменить свою роль") - + users = load_users() if username not in users: raise HTTPException(404, "Пользователь не найден") - + new_role = data.get("role") if new_role not in ["admin", "user", "support", "banned"]: raise HTTPException(400, "Неверная роль") - + # Нельзя назначить роль owner if new_role == "owner": raise HTTPException(400, "Нельзя назначить роль владельца") - + users[username]["role"] = new_role save_users(users) - + return {"message": "Роль обновлена"} + @app.get("/api/users/{username}/permissions") async def get_user_permissions(username: str, user: dict = Depends(get_current_user)): """Получить права пользователя""" # Только владелец и админы могут просматривать права if user["role"] not in ["owner", "admin"]: raise HTTPException(403, "Недостаточно прав") - + users = load_users() if username not in users: raise HTTPException(404, "Пользователь не найден") - + target_user = users[username] - + # Если у пользователя нет прав, создаем дефолтные if "permissions" not in target_user: target_user["permissions"] = { "servers": True, "tickets": True, "users": target_user["role"] in ["owner", "admin"], - "files": True + "files": True, } users[username] = target_user save_users(users) - + return { "username": username, "role": target_user["role"], - "permissions": target_user["permissions"] - } + "permissions": target_user["permissions"], + } + @app.put("/api/users/{username}/permissions") -async def update_user_permissions(username: str, data: dict, user: dict = Depends(get_current_user)): +async def update_user_permissions( + username: str, data: dict, user: dict = Depends(get_current_user) +): """Обновить права пользователя (только для владельца)""" if user["role"] != "owner": raise HTTPException(403, "Только владелец может изменять права") - + if username == user["username"]: raise HTTPException(400, "Нельзя изменить свои права") - + users = load_users() if username not in users: raise HTTPException(404, "Пользователь не найден") - + target_user = users[username] - + # Нельзя изменять права владельца if target_user["role"] == "owner": raise HTTPException(400, "Нельзя изменять права владельца") - + permissions = data.get("permissions", {}) - + # Валидация прав valid_permissions = ["servers", "tickets", "users", "files"] for perm in permissions: if perm not in valid_permissions: raise HTTPException(400, f"Неверное право: {perm}") - + # Обновляем права if "permissions" not in target_user: target_user["permissions"] = { "servers": True, "tickets": True, "users": False, - "files": True + "files": True, } - + target_user["permissions"].update(permissions) users[username] = target_user save_users(users) - - return { - "message": "Права обновлены", - "permissions": target_user["permissions"] - } + + return {"message": "Права обновлены", "permissions": target_user["permissions"]} + @app.post("/api/users/{username}/revoke-access") -async def revoke_user_access(username: str, data: dict, user: dict = Depends(get_current_user)): +async def revoke_user_access( + username: str, data: dict, user: dict = Depends(get_current_user) +): """Забрать доступ к определенным ресурсам (только для владельца)""" if user["role"] != "owner": raise HTTPException(403, "Только владелец может забирать доступ") - + users = load_users() if username not in users: raise HTTPException(404, "Пользователь не найден") - + target_user = users[username] - + # Нельзя забирать доступ у владельца if target_user["role"] == "owner": raise HTTPException(400, "Нельзя забирать доступ у владельца") - + resource_type = data.get("type") # "servers", "tickets", "all" - + if resource_type == "servers": # Забираем доступ ко всем серверам target_user["servers"] = [] if "permissions" in target_user: target_user["permissions"]["servers"] = False @@ -594,44 +647,47 @@ if "permissions" in target_user: target_user["permissions"] = { "servers": False, "tickets": False, "users": False, - "files": False + "files": False, } else: raise HTTPException(400, "Неверный тип ресурса") - + users[username] = target_user save_users(users) - + return { "message": f"Доступ к {resource_type} забран", - "permissions": target_user.get("permissions", {}) - } + "permissions": target_user.get("permissions", {}), + } + @app.post("/api/users/{username}/grant-access") -async def grant_user_access(username: str, data: dict, user: dict = Depends(get_current_user)): +async def grant_user_access( + username: str, data: dict, user: dict = Depends(get_current_user) +): """Выдать доступ к определенным ресурсам (только для владельца)""" if user["role"] != "owner": raise HTTPException(403, "Только владелец может выдавать доступ") - + users = load_users() if username not in users: raise HTTPException(404, "Пользователь не найден") - + target_user = users[username] resource_type = data.get("type") # "servers", "tickets", "files" - + if "permissions" not in target_user: target_user["permissions"] = { "servers": False, "tickets": False, "users": False, - "files": False + "files": False, } - + if resource_type == "servers": target_user["permissions"]["servers"] = True elif resource_type == "tickets": target_user["permissions"]["tickets"] = True elif resource_type == "files": @@ -639,416 +695,462 @@ elif resource_type == "all": target_user["permissions"] = { "servers": True, "tickets": True, "users": target_user["role"] in ["admin"], - "files": True + "files": True, } else: raise HTTPException(400, "Неверный тип ресурса") - + users[username] = target_user save_users(users) - + return { "message": f"Доступ к {resource_type} выдан", - "permissions": target_user["permissions"] - } + "permissions": target_user["permissions"], + } + # API для личного кабинета @app.put("/api/profile/username") async def update_username(data: dict, user: dict = Depends(get_current_user)): """Изменить имя пользователя""" new_username = data.get("new_username", "").strip() password = data.get("password", "") - + if not new_username: raise HTTPException(400, "Имя пользователя не может быть пустым") - + if len(new_username) < 3: raise HTTPException(400, "Имя пользователя должно быть не менее 3 символов") - + users = load_users() - + # Проверяем пароль if not verify_password(password, users[user["username"]]["password"]): raise HTTPException(400, "Неверный пароль") - + # Проверяем, не занято ли новое имя if new_username in users and new_username != user["username"]: raise HTTPException(400, "Это имя пользователя уже занято") - + # Сохраняем данные пользователя old_username = user["username"] user_data = users[old_username] - + # Удаляем старую запись и создаём новую del users[old_username] user_data["username"] = new_username users[new_username] = user_data - + # Обновляем владельцев серверов for server_dir in SERVERS_DIR.iterdir(): if server_dir.is_dir(): config = load_server_config(server_dir.name) if config.get("owner") == old_username: config["owner"] = new_username save_server_config(server_dir.name, config) - + # Обновляем доступы к серверам у других пользователей for username, user_info in users.items(): if "servers" in user_info and old_username in user_info.get("servers", []): - user_info["servers"] = [new_username if s == old_username else s for s in user_info["servers"]] - + user_info["servers"] = [ + new_username if s == old_username else s for s in user_info["servers"] + ] + save_users(users) - + # Создаём новый токен new_token = create_access_token({"sub": new_username, "role": user_data["role"]}) - + return { "message": "Имя пользователя изменено", "access_token": new_token, - "username": new_username - } + "username": new_username, + } + @app.put("/api/profile/password") async def update_password(data: dict, user: dict = Depends(get_current_user)): """Изменить пароль""" old_password = data.get("old_password", "") new_password = data.get("new_password", "") - + if not old_password or not new_password: raise HTTPException(400, "Заполните все поля") - + if len(new_password) < 6: raise HTTPException(400, "Новый пароль должен быть не менее 6 символов") - + users = load_users() - + # Проверяем старый пароль if not verify_password(old_password, users[user["username"]]["password"]): raise HTTPException(400, "Неверный старый пароль") - + # Устанавливаем новый пароль users[user["username"]]["password"] = get_password_hash(new_password) save_users(users) - + return {"message": "Пароль изменён"} + @app.get("/api/profile/stats") async def get_profile_stats(user: dict = Depends(get_current_user)): """Получить статистику профиля""" users = load_users() user_data = users.get(user["username"], {}) - + # Подсчитываем серверы пользователя owned_servers = [] accessible_servers = [] - + for server_dir in SERVERS_DIR.iterdir(): if server_dir.is_dir(): config = load_server_config(server_dir.name) if config.get("owner") == user["username"]: - owned_servers.append({ - "name": server_dir.name, - "displayName": config.get("displayName", server_dir.name) - }) - elif user["username"] in user_data.get("servers", []) or user["role"] == "admin": - accessible_servers.append({ - "name": server_dir.name, - "displayName": config.get("displayName", server_dir.name) - }) - + owned_servers.append( + { + "name": server_dir.name, + "displayName": config.get("displayName", server_dir.name), + } + ) + elif ( + user["username"] in user_data.get("servers", []) + or user["role"] == "admin" + ): + accessible_servers.append( + { + "name": server_dir.name, + "displayName": config.get("displayName", server_dir.name), + } + ) + # Подсчитываем тикеты tickets = load_tickets() user_tickets = [t for t in tickets.values() if t["author"] == user["username"]] - + tickets_stats = { "total": len(user_tickets), "pending": len([t for t in user_tickets if t["status"] == "pending"]), "in_progress": len([t for t in user_tickets if t["status"] == "in_progress"]), - "closed": len([t for t in user_tickets if t["status"] == "closed"]) - } - + "closed": len([t for t in user_tickets if t["status"] == "closed"]), + } + return { "username": user["username"], "role": user["role"], "owned_servers": owned_servers, "accessible_servers": accessible_servers, "tickets": tickets_stats, - "total_servers": len(owned_servers) + len(accessible_servers) - } + "total_servers": len(owned_servers) + len(accessible_servers), + } + @app.get("/api/profile/stats/{username}") async def get_user_profile_stats(username: str, user: dict = Depends(get_current_user)): """Получить статистику профиля другого пользователя (только для админов и тех. поддержки)""" # Проверка прав доступа if user["role"] not in ["admin", "support"]: - raise HTTPException(403, "Недостаточно прав для просмотра профилей других пользователей") - + raise HTTPException( + 403, "Недостаточно прав для просмотра профилей других пользователей" + ) + users = load_users() - + # Проверка существования пользователя if username not in users: raise HTTPException(404, "Пользователь не найден") - + target_user = users[username] - + # Подсчитываем серверы пользователя owned_servers = [] accessible_servers = [] - + for server_dir in SERVERS_DIR.iterdir(): if server_dir.is_dir(): config = load_server_config(server_dir.name) if config.get("owner") == username: - owned_servers.append({ - "name": server_dir.name, - "displayName": config.get("displayName", server_dir.name) - }) - elif username in target_user.get("servers", []) or target_user["role"] == "admin": - accessible_servers.append({ - "name": server_dir.name, - "displayName": config.get("displayName", server_dir.name) - }) - + owned_servers.append( + { + "name": server_dir.name, + "displayName": config.get("displayName", server_dir.name), + } + ) + elif ( + username in target_user.get("servers", []) + or target_user["role"] == "admin" + ): + accessible_servers.append( + { + "name": server_dir.name, + "displayName": config.get("displayName", server_dir.name), + } + ) + # Подсчитываем тикеты tickets = load_tickets() user_tickets = [t for t in tickets.values() if t["author"] == username] - + tickets_stats = { "total": len(user_tickets), "pending": len([t for t in user_tickets if t["status"] == "pending"]), "in_progress": len([t for t in user_tickets if t["status"] == "in_progress"]), - "closed": len([t for t in user_tickets if t["status"] == "closed"]) - } - + "closed": len([t for t in user_tickets if t["status"] == "closed"]), + } + return { "username": username, "role": target_user["role"], "owned_servers": owned_servers, "accessible_servers": accessible_servers, "tickets": tickets_stats, "total_servers": len(owned_servers) + len(accessible_servers), - "is_viewing_other": True # Флаг что это чужой профиль - } + "is_viewing_other": True, # Флаг что это чужой профиль + } + # API для серверов @app.get("/api/servers") async def get_servers(user: dict = Depends(get_current_user)): servers = [] try: # Владелец и администратор видят все серверы - can_view_all = user.get("role") in ["owner", "admin"] or user.get("permissions", {}).get("view_all_resources", False) - + can_view_all = user.get("role") in ["owner", "admin"] or user.get( + "permissions", {} + ).get("view_all_resources", False) + for server_dir in SERVERS_DIR.iterdir(): if server_dir.is_dir(): # Проверка доступа: владелец/админ видят всё, остальные только свои if not can_view_all and server_dir.name not in user.get("servers", []): continue - + config = load_server_config(server_dir.name) - + is_running = False if server_dir.name in server_processes: process = server_processes[server_dir.name] if process.poll() is None: is_running = True else: del server_processes[server_dir.name] - - servers.append({ - "name": server_dir.name, - "displayName": config.get("displayName", server_dir.name), - "status": "running" if is_running else "stopped" - }) - print(f"Найдено серверов для {user['username']} ({user.get('role', 'user')}): {len(servers)}") + + servers.append( + { + "name": server_dir.name, + "displayName": config.get("displayName", server_dir.name), + "status": "running" if is_running else "stopped", + } + ) + print( + f"Найдено серверов для {user['username']} ({user.get('role', 'user')}): {len(servers)}" + ) except Exception as e: print(f"Ошибка загрузки серверов: {e}") return servers + @app.post("/api/servers/create") async def create_server(data: dict, user: dict = Depends(get_current_user)): server_name = data.get("name", "").strip() if not server_name or not server_name.replace("_", "").replace("-", "").isalnum(): raise HTTPException(400, "Недопустимое имя сервера") - + server_path = SERVERS_DIR / server_name if server_path.exists(): raise HTTPException(400, "Сервер с таким именем уже существует") - + server_path.mkdir(parents=True) - + config = { "name": server_name, "displayName": data.get("displayName", server_name), - "startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"), - "owner": user["username"] # Сохраняем владельца + "startCommand": data.get( + "startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui" + ), + "owner": user["username"], # Сохраняем владельца } save_server_config(server_name, config) - + # Если пользователь не админ, автоматически выдаем ему доступ if user["role"] != "admin": users = load_users() if user["username"] in users: if "servers" not in users[user["username"]]: users[user["username"]]["servers"] = [] if server_name not in users[user["username"]]["servers"]: users[user["username"]]["servers"].append(server_name) save_users(users) - + return {"message": "Сервер создан", "name": server_name} + @app.get("/api/servers/{server_name}/config") async def get_server_config(server_name: str, user: dict = Depends(get_current_user)): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name if not server_path.exists(): raise HTTPException(404, "Сервер не найден") - + config = load_server_config(server_name) print(f"Загружена конфигурация для {server_name}: {config}") return config + @app.put("/api/servers/{server_name}/config") -async def update_server_config(server_name: str, config: dict, user: dict = Depends(get_current_user)): +async def update_server_config( + server_name: str, config: dict, user: dict = Depends(get_current_user) +): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name if not server_path.exists(): raise HTTPException(404, "Сервер не найден") - + if server_name in server_processes: raise HTTPException(400, "Остановите сервер перед изменением настроек") - + save_server_config(server_name, config) return {"message": "Настройки сохранены"} + @app.delete("/api/servers/{server_name}") async def delete_server(server_name: str, user: dict = Depends(get_current_user)): if user["role"] != "admin": raise HTTPException(403, "Только администраторы могут удалять серверы") - + server_path = SERVERS_DIR / server_name if not server_path.exists(): raise HTTPException(404, "Сервер не найден") - + if server_name in server_processes: raise HTTPException(400, "Остановите сервер перед удалением") - + shutil.rmtree(server_path) return {"message": "Сервер удален"} + # Управление процессами серверов async def read_server_output(server_name: str, process: subprocess.Popen): try: print(f"Начало чтения вывода для сервера {server_name}") loop = asyncio.get_event_loop() - + while True: if process.poll() is not None: - print(f"Процесс сервера {server_name} завершился с кодом {process.poll()}") + print( + f"Процесс сервера {server_name} завершился с кодом {process.poll()}" + ) break - + try: line = await loop.run_in_executor(None, process.stdout.readline) if not line: break - + line = line.strip() if line: if server_name not in server_logs: server_logs[server_name] = [] server_logs[server_name].append(line) - + if len(server_logs[server_name]) > 1000: server_logs[server_name].pop(0) except Exception as e: print(f"Ошибка чтения строки для {server_name}: {e}") await asyncio.sleep(0.1) - + except Exception as e: print(f"Ошибка чтения вывода сервера {server_name}: {e}") finally: print(f"Чтение вывода для сервера {server_name} завершено") if server_name in server_processes and process.poll() is not None: del server_processes[server_name] print(f"Сервер {server_name} удален из списка процессов") + @app.post("/api/servers/{server_name}/start") async def start_server(server_name: str, user: dict = Depends(get_current_user)): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name if not server_path.exists(): raise HTTPException(404, "Сервер не найден") - + if server_name in server_processes: raise HTTPException(400, "Сервер уже запущен") - + config = load_server_config(server_name) - start_command = config.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui") - + start_command = config.get( + "startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui" + ) + cmd_parts = start_command.split() - + try: if IS_WINDOWS: process = subprocess.Popen( cmd_parts, cwd=server_path, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, - creationflags=subprocess.CREATE_NO_WINDOW + creationflags=subprocess.CREATE_NO_WINDOW, ) else: process = subprocess.Popen( cmd_parts, cwd=server_path, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - bufsize=1 + bufsize=1, ) - + server_processes[server_name] = process server_logs[server_name] = [] - + asyncio.create_task(read_server_output(server_name, process)) - + print(f"Сервер {server_name} запущен с PID {process.pid}") return {"message": "Сервер запущен", "pid": process.pid} except Exception as e: print(f"Ошибка запуска сервера {server_name}: {e}") raise HTTPException(500, f"Ошибка запуска сервера: {str(e)}") + @app.post("/api/servers/{server_name}/stop") async def stop_server(server_name: str, user: dict = Depends(get_current_user)): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + if server_name not in server_processes: raise HTTPException(400, "Сервер не запущен") - + process = server_processes[server_name] - + try: if process.stdin and not process.stdin.closed: process.stdin.write("stop\n") process.stdin.flush() - + try: process.wait(timeout=30) except subprocess.TimeoutExpired: - print(f"Сервер {server_name} не остановился за 30 секунд, принудительное завершение") + print( + f"Сервер {server_name} не остановился за 30 секунд, принудительное завершение" + ) process.kill() process.wait() except Exception as e: print(f"Ошибка при остановке сервера {server_name}: {e}") try: @@ -1058,27 +1160,30 @@ pass finally: if server_name in server_processes: del server_processes[server_name] print(f"Сервер {server_name} остановлен") - + return {"message": "Сервер остановлен"} + @app.post("/api/servers/{server_name}/command") -async def send_command(server_name: str, command: dict, user: dict = Depends(get_current_user)): +async def send_command( + server_name: str, command: dict, user: dict = Depends(get_current_user) +): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + if server_name not in server_processes: raise HTTPException(400, "Сервер не запущен") - + process = server_processes[server_name] - + if process.poll() is not None: del server_processes[server_name] raise HTTPException(400, "Сервер не запущен") - + try: cmd = command["command"] if process.stdin and not process.stdin.closed: process.stdin.write(cmd + "\n") process.stdin.flush() @@ -1088,85 +1193,76 @@ raise HTTPException(400, "Невозможно отправить команду") except Exception as e: print(f"Ошибка отправки команды серверу {server_name}: {e}") raise HTTPException(500, f"Ошибка отправки команды: {str(e)}") + @app.get("/api/servers/{server_name}/stats") async def get_server_stats(server_name: str, user: dict = Depends(get_current_user)): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name - + try: - disk_usage = sum(f.stat().st_size for f in server_path.rglob('*') if f.is_file()) + disk_usage = sum( + f.stat().st_size for f in server_path.rglob("*") if f.is_file() + ) disk_mb = disk_usage / 1024 / 1024 except: disk_mb = 0 - + if server_name not in server_processes: - return { - "status": "stopped", - "cpu": 0, - "memory": 0, - "disk": round(disk_mb, 2) - } - + return {"status": "stopped", "cpu": 0, "memory": 0, "disk": round(disk_mb, 2)} + process = server_processes[server_name] try: if process.poll() is not None: del server_processes[server_name] return { "status": "stopped", "cpu": 0, "memory": 0, - "disk": round(disk_mb, 2) + "disk": round(disk_mb, 2), } - + proc = psutil.Process(process.pid) memory_mb = proc.memory_info().rss / 1024 / 1024 cpu_percent = proc.cpu_percent(interval=0.1) - + return { "status": "running", "cpu": round(cpu_percent, 2), "memory": round(memory_mb, 2), - "disk": round(disk_mb, 2) + "disk": round(disk_mb, 2), } except (psutil.NoSuchProcess, psutil.AccessDenied): if server_name in server_processes: del server_processes[server_name] - return { - "status": "stopped", - "cpu": 0, - "memory": 0, - "disk": round(disk_mb, 2) - } + return {"status": "stopped", "cpu": 0, "memory": 0, "disk": round(disk_mb, 2)} except Exception as e: print(f"Ошибка получения статистики для {server_name}: {e}") - return { - "status": "unknown", - "cpu": 0, - "memory": 0, - "disk": round(disk_mb, 2) - } + return {"status": "unknown", "cpu": 0, "memory": 0, "disk": round(disk_mb, 2)} + @app.websocket("/ws/servers/{server_name}/console") async def console_websocket(websocket: WebSocket, server_name: str): await websocket.accept() print(f"WebSocket подключен для сервера: {server_name}") - + if server_name in server_logs: print(f"Отправка {len(server_logs[server_name])} существующих логов") for log in server_logs[server_name]: await websocket.send_text(log) else: print(f"Логов для сервера {server_name} пока нет") - await websocket.send_text(f"[Панель] Ожидание логов от сервера {server_name}...") - + await websocket.send_text( + f"[Панель] Ожидание логов от сервера {server_name}..." + ) + last_sent_index = len(server_logs.get(server_name, [])) - + try: while True: if server_name in server_logs: current_logs = server_logs[server_name] if len(current_logs) > last_sent_index: @@ -1176,128 +1272,149 @@ await asyncio.sleep(0.1) except Exception as e: print(f"WebSocket ошибка: {e}") pass + # API для файлов @app.get("/api/servers/{server_name}/files") -async def list_files(server_name: str, path: str = "", user: dict = Depends(get_current_user)): +async def list_files( + server_name: str, path: str = "", user: dict = Depends(get_current_user) +): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name if not server_path.exists(): raise HTTPException(404, "Сервер не найден") - + target_path = server_path / path if path else server_path - + try: target_path = target_path.resolve() server_path = server_path.resolve() if not str(target_path).startswith(str(server_path)): raise HTTPException(403, "Доступ запрещен") except: raise HTTPException(404, "Путь не найден") - + if not target_path.exists(): raise HTTPException(404, "Путь не найден") - + if not target_path.is_dir(): raise HTTPException(400, "Путь не является директорией") - + files = [] try: for item in target_path.iterdir(): - files.append({ - "name": item.name, - "type": "directory" if item.is_dir() else "file", - "size": item.stat().st_size if item.is_file() else 0 - }) + files.append( + { + "name": item.name, + "type": "directory" if item.is_dir() else "file", + "size": item.stat().st_size if item.is_file() else 0, + } + ) except Exception as e: print(f"Ошибка чтения директории: {e}") raise HTTPException(500, f"Ошибка чтения директории: {str(e)}") - + return files + @app.get("/api/servers/{server_name}/files/download") -async def download_file(server_name: str, path: str, user: dict = Depends(get_current_user)): +async def download_file( + server_name: str, path: str, user: dict = Depends(get_current_user) +): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name file_path = server_path / path - + if not file_path.exists() or not str(file_path).startswith(str(server_path)): raise HTTPException(404, "Файл не найден") - + return FileResponse(file_path, filename=file_path.name) + @app.post("/api/servers/{server_name}/files/upload") -async def upload_file(server_name: str, path: str, file: UploadFile = File(...), user: dict = Depends(get_current_user)): - print(f"Upload request: server={server_name}, path='{path}', filename='{file.filename}'") - +async def upload_file( + server_name: str, + path: str, + file: UploadFile = File(...), + user: dict = Depends(get_current_user), +): + print( + f"Upload request: server={server_name}, path='{path}', filename='{file.filename}'" + ) + if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name target_path = server_path / path / file.filename - + print(f"Target path: {target_path}") print(f"Server path: {server_path}") - print(f"Path starts with server_path: {str(target_path).startswith(str(server_path))}") - + print( + f"Path starts with server_path: {str(target_path).startswith(str(server_path))}" + ) + if not str(target_path).startswith(str(server_path)): raise HTTPException(400, "Недопустимый путь") - + try: target_path.parent.mkdir(parents=True, exist_ok=True) print(f"Created directory: {target_path.parent}") except Exception as e: print(f"Error creating directory: {e}") raise HTTPException(500, f"Ошибка создания директории: {str(e)}") - + try: with open(target_path, "wb") as f: content = await file.read() f.write(content) print(f"File written successfully: {target_path}") except Exception as e: print(f"Error writing file: {e}") raise HTTPException(500, f"Ошибка записи файла: {str(e)}") - + return {"message": "Файл загружен"} + @app.post("/api/servers/{server_name}/files/create") -async def create_file_or_folder(server_name: str, data: dict, user: dict = Depends(get_current_user)): +async def create_file_or_folder( + server_name: str, data: dict, user: dict = Depends(get_current_user) +): """Создать новый файл или папку""" if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + item_type = data.get("type") # "file" or "folder" name = data.get("name", "").strip() path = data.get("path", "") # Текущая папка - + if not name: raise HTTPException(400, "Имя не может быть пустым") - + if item_type not in ["file", "folder"]: raise HTTPException(400, "Тип должен быть 'file' или 'folder'") - + server_path = SERVERS_DIR / server_name - + # Формируем полный путь if path: full_path = server_path / path / name else: full_path = server_path / name - + print(f"Creating {item_type}: {full_path}") - + # Проверка безопасности if not str(full_path).startswith(str(server_path)): raise HTTPException(400, "Недопустимый путь") - + try: if item_type == "folder": # Создаем папку full_path.mkdir(parents=True, exist_ok=True) # Создаем .gitkeep чтобы папка не была пустой @@ -1307,198 +1424,242 @@ else: # Создаем файл full_path.parent.mkdir(parents=True, exist_ok=True) full_path.touch() print(f"File created: {full_path}") - - return {"message": f"{'Папка' if item_type == 'folder' else 'Файл'} создан(а)", "path": str(full_path)} + + return { + "message": f"{'Папка' if item_type == 'folder' else 'Файл'} создан(а)", + "path": str(full_path), + } except Exception as e: print(f"Error creating {item_type}: {e}") raise HTTPException(500, f"Ошибка создания: {str(e)}") + @app.delete("/api/servers/{server_name}/files") -async def delete_file(server_name: str, path: str, user: dict = Depends(get_current_user)): +async def delete_file( + server_name: str, path: str, user: dict = Depends(get_current_user) +): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name target_path = server_path / path - + if not target_path.exists() or not str(target_path).startswith(str(server_path)): raise HTTPException(404, "Файл не найден") - + if target_path.is_dir(): shutil.rmtree(target_path) else: target_path.unlink() - + return {"message": "Файл удален"} + @app.get("/api/servers/{server_name}/files/content") -async def get_file_content(server_name: str, path: str, user: dict = Depends(get_current_user)): +async def get_file_content( + server_name: str, path: str, user: dict = Depends(get_current_user) +): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name file_path = server_path / path - - if not file_path.exists() or not file_path.is_file() or not str(file_path).startswith(str(server_path)): + + if ( + not file_path.exists() + or not file_path.is_file() + or not str(file_path).startswith(str(server_path)) + ): raise HTTPException(404, "Файл не найден") - + try: - with open(file_path, 'r', encoding='utf-8') as f: + with open(file_path, "r", encoding="utf-8") as f: content = f.read() return {"content": content} except UnicodeDecodeError: raise HTTPException(400, "Файл не является текстовым") + @app.put("/api/servers/{server_name}/files/content") -async def update_file_content(server_name: str, path: str, data: dict, user: dict = Depends(get_current_user)): +async def update_file_content( + server_name: str, path: str, data: dict, user: dict = Depends(get_current_user) +): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name file_path = server_path / path - - if not file_path.exists() or not file_path.is_file() or not str(file_path).startswith(str(server_path)): + + if ( + not file_path.exists() + or not file_path.is_file() + or not str(file_path).startswith(str(server_path)) + ): raise HTTPException(404, "Файл не найден") - + try: - with open(file_path, 'w', encoding='utf-8') as f: + with open(file_path, "w", encoding="utf-8") as f: f.write(data.get("content", "")) return {"message": "Файл сохранен"} except Exception as e: raise HTTPException(400, f"Ошибка сохранения файла: {str(e)}") + @app.put("/api/servers/{server_name}/files/rename") -async def rename_file(server_name: str, old_path: str, new_name: str, user: dict = Depends(get_current_user)): +async def rename_file( + server_name: str, + old_path: str, + new_name: str, + user: dict = Depends(get_current_user), +): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name old_file_path = server_path / old_path - - if not old_file_path.exists() or not str(old_file_path).startswith(str(server_path)): + + if not old_file_path.exists() or not str(old_file_path).startswith( + str(server_path) + ): raise HTTPException(404, "Файл не найден") - + new_file_path = old_file_path.parent / new_name - + if new_file_path.exists(): raise HTTPException(400, "Файл с таким именем уже существует") - + if not str(new_file_path).startswith(str(server_path)): raise HTTPException(400, "Недопустимое имя файла") - + old_file_path.rename(new_file_path) return {"message": "Файл переименован"} + @app.post("/api/servers/{server_name}/files/move") -async def move_file(server_name: str, data: dict, user: dict = Depends(get_current_user)): +async def move_file( + server_name: str, data: dict, user: dict = Depends(get_current_user) +): """Переместить файл или папку""" if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + source_path = data.get("source", "").strip() destination_path = data.get("destination", "").strip() - + if not source_path: raise HTTPException(400, "Не указан исходный путь") - + server_path = SERVERS_DIR / server_name source_full = server_path / source_path - + # Формируем путь назначения if destination_path: # Извлекаем имя файла из source_path file_name = source_full.name dest_full = server_path / destination_path / file_name else: # Перемещение в корень file_name = source_full.name dest_full = server_path / file_name - + print(f"Moving: {source_full} -> {dest_full}") - + # Проверки безопасности if not source_full.exists(): raise HTTPException(404, "Исходный файл не найден") - + if not str(source_full).startswith(str(server_path)): raise HTTPException(400, "Недопустимый исходный путь") - + if not str(dest_full).startswith(str(server_path)): raise HTTPException(400, "Недопустимый путь назначения") - + if dest_full.exists(): - raise HTTPException(400, "Файл с таким именем уже существует в папке назначения") - + raise HTTPException( + 400, "Файл с таким именем уже существует в папке назначения" + ) + try: # Создаем папку назначения если не существует dest_full.parent.mkdir(parents=True, exist_ok=True) - + # Перемещаем файл/папку import shutil + shutil.move(str(source_full), str(dest_full)) - + print(f"Moved successfully: {dest_full}") return {"message": "Файл перемещен", "new_path": str(dest_full)} except Exception as e: print(f"Error moving file: {e}") raise HTTPException(500, f"Ошибка перемещения: {str(e)}") + @app.put("/api/servers/{server_name}/files/rename") -async def rename_file(server_name: str, old_path: str, new_name: str, user: dict = Depends(get_current_user)): +async def rename_file( + server_name: str, + old_path: str, + new_name: str, + user: dict = Depends(get_current_user), +): if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") - + server_path = SERVERS_DIR / server_name old_file_path = server_path / old_path - - if not old_file_path.exists() or not str(old_file_path).startswith(str(server_path)): + + if not old_file_path.exists() or not str(old_file_path).startswith( + str(server_path) + ): raise HTTPException(404, "Файл не найден") - + new_file_path = old_file_path.parent / new_name - + if new_file_path.exists(): raise HTTPException(400, "Файл с таким именем уже существует") - + if not str(new_file_path).startswith(str(server_path)): raise HTTPException(400, "Недопустимое имя файла") - + old_file_path.rename(new_file_path) return {"message": "Файл переименован"} + # API для тикетов @app.get("/api/tickets") async def get_tickets(user: dict = Depends(get_current_user)): """Получить список тикетов""" # Проверяем права на тикеты if not user.get("permissions", {}).get("tickets", True): raise HTTPException(403, "Нет доступа к тикетам") - + tickets = load_tickets() - + # Владелец, админы и тех. поддержка видят все тикеты if user["role"] in ["owner", "admin", "support"]: return list(tickets.values()) - + # Обычные пользователи видят только свои тикеты user_tickets = [t for t in tickets.values() if t["author"] == user["username"]] return user_tickets + @app.post("/api/tickets/create") async def create_ticket(data: dict, user: dict = Depends(get_current_user)): """Создать новый тикет""" # Проверяем права на тикеты if not user.get("permissions", {}).get("tickets", True): raise HTTPException(403, "Нет доступа к тикетам") - + tickets = load_tickets() - + # Генерируем ID тикета ticket_id = str(len(tickets) + 1) - + ticket = { "id": ticket_id, "title": data.get("title", "").strip(), "description": data.get("description", "").strip(), "author": user["username"], @@ -1507,368 +1668,467 @@ "updated_at": datetime.utcnow().isoformat(), "messages": [ { "author": user["username"], "text": data.get("description", "").strip(), - "timestamp": datetime.utcnow().isoformat() + "timestamp": datetime.utcnow().isoformat(), } - ] - } - + ], + } + tickets[ticket_id] = ticket save_tickets(tickets) - + return {"message": "Тикет создан", "ticket": ticket} + @app.get("/api/tickets/{ticket_id}") async def get_ticket(ticket_id: str, user: dict = Depends(get_current_user)): """Получить тикет по ID""" # Проверяем права на тикеты if not user.get("permissions", {}).get("tickets", True): raise HTTPException(403, "Нет доступа к тикетам") - + tickets = load_tickets() - + if ticket_id not in tickets: raise HTTPException(404, "Тикет не найден") - + ticket = tickets[ticket_id] - + # Проверка доступа - if user["role"] not in ["owner", "admin", "support"] and ticket["author"] != user["username"]: + if ( + user["role"] not in ["owner", "admin", "support"] + and ticket["author"] != user["username"] + ): raise HTTPException(403, "Нет доступа к этому тикету") - + return ticket + @app.post("/api/tickets/{ticket_id}/message") -async def add_ticket_message(ticket_id: str, data: dict, user: dict = Depends(get_current_user)): +async def add_ticket_message( + ticket_id: str, data: dict, user: dict = Depends(get_current_user) +): """Добавить сообщение в тикет""" # Проверяем права на тикеты if not user.get("permissions", {}).get("tickets", True): raise HTTPException(403, "Нет доступа к тикетам") - + tickets = load_tickets() - + if ticket_id not in tickets: raise HTTPException(404, "Тикет не найден") - + ticket = tickets[ticket_id] - + # Проверка доступа - if user["role"] not in ["owner", "admin", "support"] and ticket["author"] != user["username"]: + if ( + user["role"] not in ["owner", "admin", "support"] + and ticket["author"] != user["username"] + ): raise HTTPException(403, "Нет доступа к этому тикету") - + message = { "author": user["username"], "text": data.get("text", "").strip(), - "timestamp": datetime.utcnow().isoformat() - } - + "timestamp": datetime.utcnow().isoformat(), + } + ticket["messages"].append(message) ticket["updated_at"] = datetime.utcnow().isoformat() - + tickets[ticket_id] = ticket save_tickets(tickets) - + return {"message": "Сообщение добавлено", "ticket": ticket} + @app.put("/api/tickets/{ticket_id}/status") -async def update_ticket_status(ticket_id: str, data: dict, user: dict = Depends(get_current_user)): +async def update_ticket_status( + ticket_id: str, data: dict, user: dict = Depends(get_current_user) +): """Изменить статус тикета (только для владельца, админов и тех. поддержки)""" if user["role"] not in ["owner", "admin", "support"]: raise HTTPException(403, "Недостаточно прав") - + # Проверяем права на тикеты if not user.get("permissions", {}).get("tickets", True): raise HTTPException(403, "Нет доступа к тикетам") - + tickets = load_tickets() - + if ticket_id not in tickets: raise HTTPException(404, "Тикет не найден") - + new_status = data.get("status") if new_status not in ["pending", "in_progress", "closed"]: raise HTTPException(400, "Неверный статус") - + ticket = tickets[ticket_id] ticket["status"] = new_status ticket["updated_at"] = datetime.utcnow().isoformat() - + # Добавляем системное сообщение о смене статуса status_names = { "pending": "На рассмотрении", "in_progress": "В работе", - "closed": "Закрыт" - } - + "closed": "Закрыт", + } + message = { "author": "system", "text": f"Статус изменён на: {status_names[new_status]}", - "timestamp": datetime.utcnow().isoformat() - } - + "timestamp": datetime.utcnow().isoformat(), + } + ticket["messages"].append(message) - + tickets[ticket_id] = ticket save_tickets(tickets) - + return {"message": "Статус обновлён", "ticket": ticket} # ============================================ # УПРАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯМИ (v1.1.0) # ============================================ + # Загрузка пользователей def load_users_dict(): users_file = Path("users.json") if not users_file.exists(): return {} with open(users_file, "r", encoding="utf-8") as f: return json.load(f) + def save_users_dict(users): with open("users.json", "w", encoding="utf-8") as f: json.dump(users, f, indent=2, ensure_ascii=False) + # Проверка прав def require_owner(current_user: dict): if current_user.get("role") != "owner": raise HTTPException(status_code=403, detail="Требуется роль владельца") + def require_admin_or_owner(current_user: dict): if current_user.get("role") not in ["owner", "admin"]: - raise HTTPException(status_code=403, detail="Требуется роль администратора или владельца") + raise HTTPException( + status_code=403, detail="Требуется роль администратора или владельца" + ) + # 1. Получить список пользователей @app.get("/api/users") async def get_users(current_user: dict = Depends(get_current_user)): require_admin_or_owner(current_user) - + users = load_users_dict() users_list = [] for username, user_data in users.items(): user_copy = user_data.copy() user_copy.pop("password", None) users_list.append(user_copy) - + return users_list + # 2. Изменить роль пользователя class RoleChange(BaseModel): role: str + @app.put("/api/users/{username}/role") -async def change_user_role(username: str, role_data: RoleChange, current_user: dict = Depends(get_current_user)): +async def change_user_role( + username: str, role_data: RoleChange, current_user: dict = Depends(get_current_user) +): require_owner(current_user) - + users = load_users_dict() - + if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") - + if username == current_user.get("username"): raise HTTPException(status_code=400, detail="Нельзя изменить свою роль") - + valid_roles = ["owner", "admin", "support", "user", "banned"] if role_data.role not in valid_roles: raise HTTPException(status_code=400, detail=f"Неверная роль") - + # Разрешаем несколько владельцев (убрано ограничение на одного) # Теперь можно назначить несколько пользователей с ролью owner - + old_role = users[username].get("role", "user") users[username]["role"] = role_data.role - + # Обновляем права if role_data.role == "owner": users[username]["permissions"] = { - "manage_users": True, "manage_roles": True, "manage_servers": True, - "manage_tickets": True, "manage_files": True, "delete_users": True, - "view_all_resources": True + "manage_users": True, + "manage_roles": True, + "manage_servers": True, + "manage_tickets": True, + "manage_files": True, + "delete_users": True, + "view_all_resources": True, } elif role_data.role == "admin": users[username]["permissions"] = { - "manage_users": True, "manage_roles": False, "manage_servers": True, - "manage_tickets": True, "manage_files": True, "delete_users": False, - "view_all_resources": True + "manage_users": True, + "manage_roles": False, + "manage_servers": True, + "manage_tickets": True, + "manage_files": True, + "delete_users": False, + "view_all_resources": True, } elif role_data.role == "support": users[username]["permissions"] = { - "manage_users": False, "manage_roles": False, "manage_servers": False, - "manage_tickets": True, "manage_files": False, "delete_users": False, - "view_all_resources": False + "manage_users": False, + "manage_roles": False, + "manage_servers": False, + "manage_tickets": True, + "manage_files": False, + "delete_users": False, + "view_all_resources": False, } elif role_data.role == "banned": users[username]["permissions"] = { - "manage_users": False, "manage_roles": False, "manage_servers": False, - "manage_tickets": False, "manage_files": False, "delete_users": False, - "view_all_resources": False + "manage_users": False, + "manage_roles": False, + "manage_servers": False, + "manage_tickets": False, + "manage_files": False, + "delete_users": False, + "view_all_resources": False, } else: # user users[username]["permissions"] = { - "manage_users": False, "manage_roles": False, "manage_servers": True, - "manage_tickets": True, "manage_files": True, "delete_users": False, - "view_all_resources": False + "manage_users": False, + "manage_roles": False, + "manage_servers": True, + "manage_tickets": True, + "manage_files": True, + "delete_users": False, + "view_all_resources": False, } - + save_users_dict(users) - - return {"message": f"Роль изменена с {old_role} на {role_data.role}", "user": {"username": username, "role": role_data.role}} + + return { + "message": f"Роль изменена с {old_role} на {role_data.role}", + "user": {"username": username, "role": role_data.role}, + } + # 3. Заблокировать пользователя class BanRequest(BaseModel): reason: str = "Заблокирован администратором" + @app.post("/api/users/{username}/ban") -async def ban_user(username: str, ban_data: BanRequest, current_user: dict = Depends(get_current_user)): +async def ban_user( + username: str, ban_data: BanRequest, current_user: dict = Depends(get_current_user) +): require_admin_or_owner(current_user) - + users = load_users_dict() - + if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") - + if username == current_user.get("username"): raise HTTPException(status_code=400, detail="Нельзя заблокировать самого себя") - + # Проверяем, что не блокируем последнего владельца if users[username].get("role") == "owner": owners_count = sum(1 for u in users.values() if u.get("role") == "owner") if owners_count <= 1: - raise HTTPException(status_code=400, detail="Нельзя заблокировать последнего владельца. Должен остаться хотя бы один владелец.") - + raise HTTPException( + status_code=400, + detail="Нельзя заблокировать последнего владельца. Должен остаться хотя бы один владелец.", + ) + users[username]["role"] = "banned" users[username]["permissions"] = { - "manage_users": False, "manage_roles": False, "manage_servers": False, - "manage_tickets": False, "manage_files": False, "delete_users": False, - "view_all_resources": False + "manage_users": False, + "manage_roles": False, + "manage_servers": False, + "manage_tickets": False, + "manage_files": False, + "delete_users": False, + "view_all_resources": False, } users[username]["ban_reason"] = ban_data.reason - + save_users_dict(users) - - return {"message": f"Пользователь {username} заблокирован", "username": username, "reason": ban_data.reason} + + return { + "message": f"Пользователь {username} заблокирован", + "username": username, + "reason": ban_data.reason, + } + # 4. Разблокировать пользователя @app.post("/api/users/{username}/unban") async def unban_user(username: str, current_user: dict = Depends(get_current_user)): require_admin_or_owner(current_user) - + users = load_users_dict() - + if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") - + if users[username].get("role") != "banned": raise HTTPException(status_code=400, detail="Пользователь не заблокирован") - + users[username]["role"] = "user" users[username]["permissions"] = { - "manage_users": False, "manage_roles": False, "manage_servers": True, - "manage_tickets": True, "manage_files": True, "delete_users": False, - "view_all_resources": False + "manage_users": False, + "manage_roles": False, + "manage_servers": True, + "manage_tickets": True, + "manage_files": True, + "delete_users": False, + "view_all_resources": False, } users[username].pop("ban_reason", None) - + save_users_dict(users) - + return {"message": f"Пользователь {username} разблокирован", "username": username} + # 5. Удалить пользователя @app.delete("/api/users/{username}") async def delete_user(username: str, current_user: dict = Depends(get_current_user)): require_owner(current_user) - + users = load_users_dict() - + if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") - + if username == current_user.get("username"): raise HTTPException(status_code=400, detail="Нельзя удалить самого себя") - + # Проверяем, что не удаляем последнего владельца if users[username].get("role") == "owner": owners_count = sum(1 for u in users.values() if u.get("role") == "owner") if owners_count <= 1: - raise HTTPException(status_code=400, detail="Нельзя удалить последнего владельца. Должен остаться хотя бы один владелец.") - + raise HTTPException( + status_code=400, + detail="Нельзя удалить последнего владельца. Должен остаться хотя бы один владелец.", + ) + del users[username] save_users_dict(users) - + return {"message": f"Пользователь {username} удалён", "username": username} + # 6. Выдать доступ к серверу class ServerAccess(BaseModel): server_name: str + @app.post("/api/users/{username}/access/servers") -async def grant_server_access(username: str, access: ServerAccess, current_user: dict = Depends(get_current_user)): +async def grant_server_access( + username: str, access: ServerAccess, current_user: dict = Depends(get_current_user) +): require_admin_or_owner(current_user) - + users = load_users_dict() - + if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") - + if "resource_access" not in users[username]: users[username]["resource_access"] = {"servers": [], "tickets": [], "files": []} - + if access.server_name not in users[username]["resource_access"]["servers"]: users[username]["resource_access"]["servers"].append(access.server_name) - + # Также добавляем в старое поле servers для совместимости if "servers" not in users[username]: users[username]["servers"] = [] if access.server_name not in users[username]["servers"]: users[username]["servers"].append(access.server_name) - + save_users_dict(users) - - return {"message": f"Доступ к серверу {access.server_name} выдан", "server": access.server_name, "user": username} + + return { + "message": f"Доступ к серверу {access.server_name} выдан", + "server": access.server_name, + "user": username, + } + # 7. Забрать доступ к серверу @app.delete("/api/users/{username}/access/servers/{server_name}") -async def revoke_server_access(username: str, server_name: str, current_user: dict = Depends(get_current_user)): +async def revoke_server_access( + username: str, server_name: str, current_user: dict = Depends(get_current_user) +): require_admin_or_owner(current_user) - + users = load_users_dict() - + if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") - - if "resource_access" in users[username] and "servers" in users[username]["resource_access"]: + + if ( + "resource_access" in users[username] + and "servers" in users[username]["resource_access"] + ): if server_name in users[username]["resource_access"]["servers"]: users[username]["resource_access"]["servers"].remove(server_name) - + # Также удаляем из старого поля servers if "servers" in users[username] and server_name in users[username]["servers"]: users[username]["servers"].remove(server_name) - + save_users_dict(users) - - return {"message": f"Доступ к серверу {server_name} отозван", "server": server_name, "user": username} + + return { + "message": f"Доступ к серверу {server_name} отозван", + "server": server_name, + "user": username, + } + # 8. Изменить права пользователя class PermissionsUpdate(BaseModel): permissions: dict + @app.put("/api/users/{username}/permissions") -async def update_user_permissions(username: str, perms: PermissionsUpdate, current_user: dict = Depends(get_current_user)): +async def update_user_permissions( + username: str, + perms: PermissionsUpdate, + current_user: dict = Depends(get_current_user), +): require_owner(current_user) - + users = load_users_dict() - + if username not in users: raise HTTPException(status_code=404, detail="Пользователь не найден") - + users[username]["permissions"] = perms.permissions save_users_dict(users) - - return {"message": f"Права пользователя {username} обновлены", "permissions": perms.permissions} + + return { + "message": f"Права пользователя {username} обновлены", + "permissions": perms.permissions, + } if __name__ == "__main__": import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) Oh no! 💥 💔 💥 6 files would be reformatted.