Commit Graph

11 Commits

Author SHA1 Message Date
ruslan 46e5b5a41e Add pilots: invite-only products with per-user RDP slots
New Service.is_pilot flag - a pilot is a Service (type RDP) that:
- is excluded from /api/public/services-by-category and
  /api/request-more-access (admin-granted only, no self-service)
- still shows as a locked card on the dashboard for users without
  access (Доступно по запросу section), but with a По приглашению
  badge instead of the self-request button/flow
- gets its own non-clickable teaser row on the public /login page
  (logos only, informational)

RdpSlot.assigned_user_id (nullable) - pilot slots are bound to one
specific user instead of being drawn from the shared pool; regular
RDP services are unaffected (field stays NULL, same pool behaviour
as before). The /go/ allocator branches on service.is_pilot to pick
the caller's assigned slot instead of any free one. Slots release
automatically (cleanup_loop) when the owning grant is revoked or
expires, and immediately on manual ACL revoke.

UserServiceAccess.expires_at (nullable) - per-grant access window,
used by pilots so their access can be shorter than the account's own
expires_at; NULL (unchanged default) means "follow the account".
has_access() and the dashboard's granted/locked split both honour it.

Admin UI: "Это пилот" checkbox on the RDP service form, an
assign-user dropdown on a pilot's slot table (replaces the
occupied-by column), and a per-pilot expiry date field in the user
ACL grid.

Schema is applied via the existing ensure_schema_compatibility()
idempotent ALTER TABLE pattern (no alembic in this project) - no
manual migration step needed, it runs at container startup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-02 06:32:05 +00:00
ruslan 40de4d5a0f Bundles feature WIP + fix request-access products cap (20 -> 200)
- app/main.py: raise products_raw[:20] cap to [:200] in /api/request-access
  and /api/request-more-access - was silently truncating access requests
  with more than 20 selected products
- accumulated bundles/product-page WIP (config, models, runtime, templates)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:58:09 +00:00
ruslan 53e18d92ac refactor: extract _dispatch_post to remove duplicate retry logic 2026-07-23 11:14:25 +00:00
ruslan 2e0d4c78ca refactor: merge duplicate pool status functions into _get_pool_status 2026-07-23 11:13:17 +00:00
ruslan 1e6eadeed2 refactor: remove dead code (safe cleanup)
- main.py: remove duplicate sqlalchemy select import
- main.py: remove unused json import in _telegram_poll_loop
- runtime.py: remove unreachable return after raise LockTimeoutError
- runtime.py: remove _restart_rdp_slot_bg (defined but never called)
2026-07-23 11:07:48 +00:00
ruslan b2297c7773 fix: route_ready rejects 5xx responses; remove spinner from wait page
route_ready returned True on 502 Bad Gateway, causing premature redirect
before the container was actually serving. Now only accepts < 500 responses.
Spinner removed from wait page per user request.
2026-07-23 10:48:16 +00:00
ruslan c58ed04821 fix: recycle webpool container on TERMINATED sessions too
terminate_session_record was explicitly skipping WEBPOOLIDX containers.
Now stops portal-webpool-N in a background thread on any session close,
so ensure_web_pool recreates it fresh for the next user.
2026-07-22 08:10:20 +00:00
ruslan ca5d1af2b5 fix: webpool containers now stay alive 24h; recycle on session expiry
- Pool containers got IDLE_TIMEOUT=86400 instead of SESSION_IDLE_SECONDS (300s)
  to prevent them from cycling every 5 minutes while idle
- On session expiry, WEBPOOLIDX containers are now stopped so ensure_web_pool()
  recreates them fresh for the next user (prevents dirty Chromium state)
2026-07-21 13:53:18 +00:00
ruslan efa1c26e5d Email improvements: domain-aware portal URL, embedded logo, fix product list color
- Store request origin domain in PendingAccessRequest.portal_url
- Use per-request portal URL in approval/rejection emails
- Embed logo as base64 so it displays without external image loading
- Fix 'Предоставлен доступ к продуктам' text color to match body color
- Switch Telegram polling to 30-second interval with single-worker flock fix
2026-05-29 16:10:40 +00:00
ruslan 58cb8b1035 feat: on-demand RDP - connect xfreerdp only when session opens
Replaces always-on xfreerdp with on-demand model (load 12 to under 1 at idle).
- rdp-proxy/manager.py: HTTP server port 7001 managing xfreerdp lifecycle
- rdp-proxy/entrypoint.sh: starts Xvfb+x11vnc+websockify+manager, no auto-connect
- rdp-proxy/Dockerfile: adds python3, copies manager.py, exposes 7001
- runtime.py: connect_rdp_slot and disconnect_rdp_slot via manager HTTP API
- terminate_session_record: disconnect instead of container restart
- main.py: calls connect_rdp_slot in background thread on session create
- maintenance.py: cleanup_loop disconnects on expire, run_maintenance_service
  includes RDP slot init, maintenance_runner fixed to import maintenance
2026-05-01 10:12:52 +00:00
ruslan c8c77048c7 refactor: split main.py into modules (config, database, models, utils, auth, runtime, maintenance)
main.py was ~3000 lines with models, routes, Docker ops, maintenance all mixed.
Split into 7 focused modules:
- config.py: env vars and constants
- database.py: SQLAlchemy engine, SessionLocal, Base, get_db
- models.py: ORM models and enums
- utils.py: logging, formatting, icon handling, misc helpers
- auth.py: password hashing, cookies, CSRF, user dependency
- runtime.py: all Docker operations, pool management, session lifecycle
- maintenance.py: cleanup loop, schema bootstrap, startup logic
- main.py: FastAPI app, middleware, all route handlers only
2026-05-01 09:40:06 +00:00