Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v5

- name: Setup Python
if: matrix.language == 'python'
uses: actions/setup-python@v6
with:
python-version: '3.13'

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
Expand Down
58 changes: 58 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
*.egg
*.egg-info/
dist/
build/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.whl
.installed.cfg

# Virtual environments
.venv/
venv/
env/
ENV/

# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/

# IDEs
.idea/
.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Logs
*.log

# Environment variables
.env
.env.*
!.env.example

# Node
node_modules/
npm-debug.log*

# Distribution
*.tar.gz
*.zip
37 changes: 18 additions & 19 deletions backend/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,37 @@
import os
import bcrypt
import secrets
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional

from jose import JWTError, jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

logger = logging.getLogger(__name__)


def _resolve_secret_key() -> tuple[str, bool]:
configured = str(os.getenv("SECRET_KEY") or "").strip()
if configured:
return configured, False

fallback_path = Path(
os.getenv(
"SECRET_KEY_FILE",
str(Path(os.getenv("TEMP") or "/tmp") / "codeai_jwt_secret.key"),
)
configured_file = str(os.getenv("SECRET_KEY_FILE") or "").strip()
if configured_file:
fallback_path = Path(configured_file).expanduser()
try:
if fallback_path.exists() and fallback_path.is_file():
cached_secret = fallback_path.read_text(encoding="utf-8").strip()
if cached_secret:
return cached_secret, True
except Exception:
pass
Comment on lines +21 to +30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider logging or surfacing failures to load SECRET_KEY_FILE rather than silently falling back to an ephemeral secret.

If SECRET_KEY_FILE is set but unreadable (bad path, permissions, etc.), this will silently fall back to an ephemeral secret. In production this means operators think a stable secret is configured, but tokens will still rotate on restart. Please emit at least a warning (including the path and exception) before falling back so misconfigurations are visible while keeping the behavior of not auto-creating/writing a key file.

Suggested implementation:

import logging


logger = logging.getLogger(__name__)


def _resolve_secret_key() -> tuple[str, bool]:
    configured_file = str(os.getenv("SECRET_KEY_FILE") or "").strip()
    if configured_file:
        fallback_path = Path(configured_file).expanduser()
        try:
            if fallback_path.exists() and fallback_path.is_file():
                cached_secret = fallback_path.read_text(encoding="utf-8").strip()
                if cached_secret:
                    return cached_secret, True
                logger.warning(
                    "SECRET_KEY_FILE '%s' is empty; falling back to ephemeral secret key",
                    fallback_path,
                )
            else:
                logger.warning(
                    "SECRET_KEY_FILE is set to '%s' but the file does not exist or is not a regular file; "
                    "falling back to an ephemeral secret key",
                    fallback_path,
                )
        except Exception as exc:
            logger.warning(
                "Failed to read SECRET_KEY_FILE '%s': %s; falling back to an ephemeral secret key",
                fallback_path,
                exc,
            )

If backend/auth.py does not currently import logging elsewhere, you may prefer to add the import near the top of the file instead of immediately before _resolve_secret_key:

<<<<<<< SEARCH
import os

import os
import logging

REPLACE

In that case, remove the import logging line from the _resolve_secret_key insertion and keep only the logger = logging.getLogger(__name__) definition at module scope (ideally near the imports).


logger.error(
"SECRET_KEY/SECRET_KEY_FILE is not configured; generating ephemeral runtime secret that invalidates tokens on restart."
)
try:
fallback_path.parent.mkdir(parents=True, exist_ok=True)
if fallback_path.exists():
cached_secret = fallback_path.read_text(encoding="utf-8").strip()
if cached_secret:
return cached_secret, True
generated_secret = secrets.token_urlsafe(48)
fallback_path.write_text(generated_secret, encoding="utf-8")
return generated_secret, True
except Exception:
return secrets.token_urlsafe(48), True
return secrets.token_urlsafe(48), True
Comment on lines +32 to +35


SECRET_KEY, SECRET_KEY_IS_RUNTIME_FALLBACK = _resolve_secret_key()
Expand All @@ -42,7 +42,6 @@ def _resolve_secret_key() -> tuple[str, bool]:
)

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
logger = logging.getLogger(__name__)


def get_password_hash(password: str) -> str:
Expand All @@ -68,7 +67,7 @@ def create_access_token(
) -> str:
to_encode = data.copy()
if not no_expiry:
expire = datetime.utcnow() + (
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
)
to_encode.update({"exp": expire})
Expand Down
25 changes: 12 additions & 13 deletions backend/auth_router.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from secrets import compare_digest, randbelow, token_urlsafe
import base64
import os
Expand All @@ -7,7 +7,7 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordRequestForm
from typing import Optional, cast
from pydantic import BaseModel, EmailStr
from pydantic import BaseModel, ConfigDict, EmailStr
from sqlalchemy.orm import Session
from webauthn import (
generate_authentication_options,
Expand Down Expand Up @@ -82,8 +82,7 @@ class UserResponse(BaseModel):
business_registration_number: Optional[str] = None
representative_name: Optional[str] = None

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)


class Token(BaseModel):
Expand Down Expand Up @@ -254,7 +253,7 @@ class PasswordRecoveryResetResponse(BaseModel):


def _issue_recovery_token(prefix: str) -> tuple[str, datetime]:
expires_at = datetime.utcnow() + timedelta(minutes=10)
expires_at = datetime.now(timezone.utc) + timedelta(minutes=10)
return f"{prefix}_{token_urlsafe(24)}", expires_at


Expand All @@ -273,7 +272,7 @@ def _normalize_password_recovery_scope(scope: str) -> str:


def _purge_expired_password_recovery_sessions() -> None:
now = datetime.utcnow()
now = datetime.now(timezone.utc)
expired_tokens = [
session_token
for session_token, session_state in _password_recovery_store.items()
Expand Down Expand Up @@ -380,7 +379,7 @@ def start_passkey_registration(
"user_id": int(user.id),
"challenge": _to_base64url(options.challenge),
"device_label": str(payload.device_label or "이 기기 패스키").strip() or "이 기기 패스키",
"expires_at": datetime.utcnow() + timedelta(minutes=5),
"expires_at": datetime.now(timezone.utc) + timedelta(minutes=5),
"user_handle": user_handle,
"rp_id": rp_id,
"expected_origin": expected_origin,
Expand All @@ -403,7 +402,7 @@ def finish_passkey_registration(
raise HTTPException(status_code=404, detail="패스키 등록 세션을 찾을 수 없습니다")

expires_at = state.get("expires_at")
if not isinstance(expires_at, datetime) or expires_at <= datetime.utcnow():
if not isinstance(expires_at, datetime) or expires_at <= datetime.now(timezone.utc):
_passkey_registration_store.pop(payload.registration_token, None)
raise HTTPException(status_code=410, detail="패스키 등록 세션이 만료되었습니다")

Expand All @@ -430,7 +429,7 @@ def finish_passkey_registration(
user.passkey_public_key = _to_base64url(verification.credential_public_key)
user.passkey_device_label = str(state.get("device_label") or "이 기기 패스키")
user.passkey_sign_count = int(verification.sign_count)
user.passkey_registered_at = datetime.utcnow()
user.passkey_registered_at = datetime.now(timezone.utc)
db.add(user)
db.commit()
_passkey_registration_store.pop(payload.registration_token, None)
Expand Down Expand Up @@ -465,7 +464,7 @@ def start_passkey_login(
)
_passkey_login_store[str(user.email)] = {
"challenge": _to_base64url(options.challenge),
"expires_at": datetime.utcnow() + timedelta(minutes=5),
"expires_at": datetime.now(timezone.utc) + timedelta(minutes=5),
"credential_id": str(user.passkey_credential_id),
"rp_id": rp_id,
"expected_origin": expected_origin,
Expand All @@ -492,7 +491,7 @@ def finish_passkey_login(
raise HTTPException(status_code=404, detail="패스키 로그인 세션을 찾을 수 없습니다")

expires_at = state.get("expires_at")
if not isinstance(expires_at, datetime) or expires_at <= datetime.utcnow():
if not isinstance(expires_at, datetime) or expires_at <= datetime.now(timezone.utc):
_passkey_login_store.pop(str(user.email), None)
raise HTTPException(status_code=410, detail="패스키 로그인 세션이 만료되었습니다")

Expand Down Expand Up @@ -581,7 +580,7 @@ def verify_password_recovery_identity(payload: PasswordRecoveryVerifyIdentityReq
raise HTTPException(status_code=404, detail="복구 세션을 찾을 수 없습니다")

expires_at = session_state.get("expires_at")
if not isinstance(expires_at, datetime) or expires_at <= datetime.utcnow():
if not isinstance(expires_at, datetime) or expires_at <= datetime.now(timezone.utc):
_password_recovery_store.pop(payload.recovery_session_token, None)
raise HTTPException(status_code=410, detail="복구 세션이 만료되었습니다")

Expand Down Expand Up @@ -638,7 +637,7 @@ def reset_password_via_recovery(
raise HTTPException(status_code=403, detail="본인확인 검증이 완료되지 않았습니다")

reset_expires_at = session_state.get("reset_expires_at")
if not isinstance(reset_expires_at, datetime) or reset_expires_at <= datetime.utcnow():
if not isinstance(reset_expires_at, datetime) or reset_expires_at <= datetime.now(timezone.utc):
_password_recovery_store.pop(session_token, None)
raise HTTPException(status_code=410, detail="재설정 토큰이 만료되었습니다")

Expand Down
41 changes: 30 additions & 11 deletions backend/llm/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2684,6 +2684,7 @@ class OrchestrationAcceptedResponse(BaseModel):


_ORCHESTRATION_PROGRESS_STORE: Dict[str, Dict[str, Any]] = {}
_ORCHESTRATION_PROGRESS_FILE_LOCK = threading.Lock()


def _runtime_progress_root() -> Path:
Expand All @@ -2694,9 +2695,8 @@ def _runtime_progress_root() -> Path:
return progress_root


def _orchestration_progress_path(run_id: str) -> Path:
safe_run_id = re.sub(r"[^a-zA-Z0-9_.-]+", "-", str(run_id or "unknown")).strip("-") or "unknown"
return _runtime_progress_root() / f"{safe_run_id}.json"
def _orchestration_progress_store_path() -> Path:
return _runtime_progress_root() / "progress_store.json"


def _build_progress_poll_url(run_id: str) -> str:
Expand All @@ -2712,23 +2712,42 @@ def _save_orchestration_progress(run_id: str, payload: Dict[str, Any]) -> Dict[s
normalized["run_id"] = str(run_id or normalized.get("run_id") or "")
normalized.setdefault("updated_at", datetime.utcnow().isoformat() + "Z")
_ORCHESTRATION_PROGRESS_STORE[normalized["run_id"]] = normalized
progress_path = _orchestration_progress_path(normalized["run_id"])
progress_path.write_text(json.dumps(normalized, ensure_ascii=False, indent=2), encoding="utf-8")
progress_path = _orchestration_progress_store_path()
with _ORCHESTRATION_PROGRESS_FILE_LOCK:
persisted_payload: Dict[str, Any] = {}
try:
if progress_path.exists() and progress_path.is_file():
Comment on lines +2715 to +2719
existing_payload = json.loads(progress_path.read_text(encoding="utf-8"))
if isinstance(existing_payload, dict):
persisted_payload = dict(existing_payload)
except Exception:
logger.warning(
"Failed to read orchestration progress store from %s before write",
Comment on lines +2716 to +2725
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Guard the write to the progress store file with error handling to avoid bubbling I/O failures into callers.

Because write_text is now on the hot path for each update, transient filesystem issues (disk full, permissions, temporary unavailability) could raise and disrupt orchestration. Please wrap the full read/modify/write block in a try/except that logs the failure but still returns normalized, relying on the already-updated in-memory _ORCHESTRATION_PROGRESS_STORE to keep the flow running.

str(progress_path),
exc_info=True,
)
persisted_payload = {}
persisted_payload[normalized["run_id"]] = normalized
progress_path.write_text(json.dumps(persisted_payload, ensure_ascii=False, indent=2), encoding="utf-8")
return normalized


def _load_orchestration_progress(run_id: str) -> Dict[str, Any]:
cached = _ORCHESTRATION_PROGRESS_STORE.get(str(run_id or ""))
if isinstance(cached, dict) and cached:
return dict(cached)
progress_path = _orchestration_progress_path(run_id)
progress_path = _orchestration_progress_store_path()
try:
if progress_path.exists() and progress_path.is_file():
payload = json.loads(progress_path.read_text(encoding="utf-8"))
if isinstance(payload, dict):
_ORCHESTRATION_PROGRESS_STORE[str(run_id or "")] = dict(payload)
return dict(payload)
with _ORCHESTRATION_PROGRESS_FILE_LOCK:
if progress_path.exists() and progress_path.is_file():
payload = json.loads(progress_path.read_text(encoding="utf-8"))
if isinstance(payload, dict):
stored = payload.get(str(run_id or ""))
if isinstance(stored, dict):
_ORCHESTRATION_PROGRESS_STORE[str(run_id or "")] = dict(stored)
return dict(stored)
except Exception:
logger.error("Failed to load orchestration progress for run_id=%s", str(run_id or ""), exc_info=True)
return {}
return {}

Expand Down
Loading
Loading