Converts Spring Boot 4.0.3 ARMA Server Web GUI to FastAPI/Python. Each phase file is fully self-contained: lists Java source files to read, output files to create, implementation patterns, REST endpoint contracts, and a completion checklist. A future agent can execute any single phase without rescanning the Java project. Phases: - 01: Foundation — SQLAlchemy models, Alembic, settings, base schemas - 02: Auth & Users — JWT middleware, RBAC, user CRUD - 03: CFG parser + server process — server.cfg round-trip, start/stop - 04: Server settings — general/network/logging/security/difficulty - 05: Mod management — mod CRUD, presets, settings, WebSocket progress - 06: Steam integration — SteamCMD queue, Workshop API, python-a2s - 07: Missions, CDLC, Discord, APScheduler jobs - 08: Middleware & polish — global exception handler, SPA redirect, structlog - 09: Testing — pytest-asyncio, respx, 80% coverage target
7.8 KiB
Phase 2 — Authentication + Security
Status: PENDING
Depends on: Phase 1 complete
Next phase: phase-03-cfg-parser-process.md
Goal
Working JWT login/logout, BCrypt password verification, the full AswgAuthority enum (37 permissions), JWT middleware enforcing auth on all /api/** routes, user CRUD service, and the auth + user REST endpoints.
After this phase: POST /api/v1/auth returns a JWT token; all other /api/** endpoints return 401 without a valid token.
Java Source Files to Read
<JAVA_SRC> = E:\TestScript\ARMA-Server-Web-Gui\src\main\java\pl\bartlomiejstepien\armaserverwebgui\
| File | What to extract |
|---|---|
<JAVA_SRC>application/auth/AuthService.java |
authenticate(), logout() logic |
<JAVA_SRC>application/auth/JwtToken.java |
JWT payload fields |
<JAVA_SRC>application/security/AswgAuthority.java |
All 37 authority code strings |
<JAVA_SRC>application/security/jwt/JwtService.java |
createJwt(), validateJwt(), blacklisting logic |
<JAVA_SRC>application/security/jwt/filter/JwtFilter.java |
Permit-all path list |
<JAVA_SRC>application/security/jwt/cleaner/InvalidJwtCleaner.java |
Scheduled cleanup logic |
<JAVA_SRC>application/security/AuthenticationFacade.java |
getCurrentUser() pattern |
<JAVA_SRC>application/security/AswgAuthenticationEntryPoint.java |
401 response shape |
<JAVA_SRC>application/config/SecurityConfig.java |
Dual enabled/disabled modes, permit-all paths |
<JAVA_SRC>domain/user/UserService.java |
All method signatures |
<JAVA_SRC>domain/user/UserServiceImpl.java |
Full implementation |
<JAVA_SRC>domain/user/dto/ (all files) |
DTO field names |
<JAVA_SRC>web/AuthRestController.java |
Exact paths, request/response JSON |
<JAVA_SRC>web/UserRestController.java |
Exact paths, request/response JSON |
<JAVA_SRC>web/request/PasswordChangeRequest.java |
Fields |
Output Files to Create
src/application/__init__.py
src/application/security/__init__.py
src/application/security/jwt_service.py
src/application/security/auth_service.py
src/application/security/permissions.py (AswgAuthority enum + has_permission factory)
src/application/security/authentication.py (get_current_user dependency)
src/application/security/password.py (BCrypt wrapper via passlib)
src/application/middleware/__init__.py
src/application/middleware/jwt_middleware.py
src/domain/__init__.py
src/domain/user/__init__.py
src/domain/user/user_service.py
src/domain/user/user_loader_service.py
src/domain/user/user_session_service.py
src/domain/user/models.py
src/web/__init__.py
src/web/schemas/__init__.py
src/web/schemas/common.py (BaseSchema with camelCase alias, RestErrorResponse)
src/web/schemas/auth.py
src/web/schemas/users.py
src/web/auth_router.py
src/web/user_router.py
Also update src/main.py:
- Register
auth_routeranduser_router - Add
JwtMiddleware - Create default user on startup in
lifespan
Implementation Notes
src/web/schemas/common.py — BaseSchema (used by ALL schemas in all phases)
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
class BaseSchema(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
class RestErrorResponse(BaseSchema):
code: str
message: str
Every schema in every phase must inherit BaseSchema to preserve camelCase JSON for the Angular frontend.
src/application/security/permissions.py
Extract all 37 codes from AswgAuthority.java. Example:
from enum import StrEnum
from fastapi import Depends, HTTPException
class AswgAuthority(StrEnum):
SERVER_START_STOP = "SERVER_START_STOP"
GENERAL_SETTINGS_VIEW = "GENERAL_SETTINGS_VIEW"
# ... all 37 values
def has_permission(authority: AswgAuthority):
async def _check(current_user=Depends(get_current_user)):
user_codes = {a.code for a in current_user.authorities}
if authority.value not in user_codes:
raise HTTPException(status_code=403)
return current_user
return Depends(_check)
Usage: @router.get("/properties", dependencies=[has_permission(AswgAuthority.GENERAL_SETTINGS_VIEW)])
src/application/security/jwt_service.py
Algorithm: HS256. JWT payload fields (from JwtToken.java): sub (username), authorities (list of code strings), iss, iat, exp.
Blacklisting: on logout, insert token into invalid_jwt_token table. On every validation, check the table. Scheduled cleanup (InvalidJwtCleaner equivalent): delete rows where expiration_datetime < now().
src/application/middleware/jwt_middleware.py
Permit-all paths from SecurityConfig.java / JwtFilter.java:
PERMIT_ALL_EXACT = {
("POST", "/api/v1/auth"),
("POST", "/api/v1/auth/logout"),
("GET", "/api/v1/actuator/health"),
("GET", "/api/v1/actuator/info"),
}
PERMIT_ALL_PREFIX = ["/api/v1/ws/", "/api/v1/logging/logs-sse"]
When settings.security_enabled = False: skip all JWT checks, attach a superuser principal to request.state.user.
On JWT failure return JSON matching AswgAuthenticationEntryPoint:
- Expired token:
{"code":"AUTH_TOKEN_EXPIRED","message":"..."}HTTP 401 - Missing token:
{"code":"AUTH_TOKEN_REQUIRED","message":"..."}HTTP 401 - Bad token:
{"code":"BAD_AUTH_TOKEN","message":"..."}HTTP 401
Auth REST endpoints (from AuthRestController.java)
POST /api/v1/auth body: {username, password} → {token: "..."} 200
GET /api/v1/auth/myself (authenticated) → UserProfileResponse 200
POST /api/v1/auth/logout (authenticated) → 200 (no body)
User REST endpoints (from UserRestController.java)
GET /api/v1/users → list[UserResponse]
POST /api/v1/users body: CreateUserRequest → UserResponse 201
PUT /api/v1/users/{id} body: UpdateUserRequest → UserResponse 200
DELETE /api/v1/users/{id} → 200 (no body)
POST /api/v1/users/{id}/password-change body: {password} → 200 (no body)
Default user creation in lifespan (src/main.py update)
async with SessionLocal() as session:
user_repo = UserRepository(session)
existing = await user_repo.find_by_username(settings.default_user_username)
if not existing:
hashed = password_service.encode(settings.default_user_password)
await user_repo.create(username=settings.default_user_username,
password=hashed, authorities=list(AswgAuthority))
elif settings.default_user_reset:
hashed = password_service.encode(settings.default_user_password)
await user_repo.update_password(existing.id, hashed)
Completion Checklist
AswgAuthorityenum has exactly 37 permission codes (count against Java source)has_permission()factory works as aDepends()- JWT is created and validated with
HS256 - Expired and blacklisted tokens are rejected with correct error JSON
- JWT middleware blocks all
/api/**without a valid token - Permit-all paths respond without token (health, login, logout, SSE, WS)
security_enabled=Falsebypasses all auth checksPOST /api/v1/auth→ JWT for valid credentials, 401 for badGET /api/v1/auth/myself→ current user profile JSONPOST /api/v1/auth/logout→ blacklists token- All user CRUD endpoints return correct status codes
- Default user created on first startup
- Password reset works when
aswg.default-user.reset=true
Contract for Phase 3
Phase 3 imports:
from src.application.security.permissions import has_permission, AswgAuthorityfrom src.application.security.authentication import CurrentUser, get_current_userfrom src.web.schemas.common import BaseSchema, RestErrorResponse