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
39 changes: 39 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Node
frontend/node_modules
frontend/dist
frontend/dist-ssr
frontend/coverage
frontend/.vite

# Python
backend/.venv
backend/venv
backend/data
backend/htmlcov
backend/.pytest_cache
backend/.ruff_cache
backend/.mypy_cache
backend/.coverage
**/__pycache__
*.egg-info

# Git / editor / AI assistant
.git
.gitignore
.claude
CLAUDE.md
.vscode
.idea

# Docs (backend/README.md is needed by the Dockerfile's COPY, keep it)
docs
CHANGELOG.md
CONTRIBUTING.md
*.md
!backend/README.md

# Docker volume / runtime data
config
*.db
*.db-journal
*.log
12 changes: 6 additions & 6 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Build and Publish Docker Image
on:
push:
branches: [master]
tags: ['v*.*.*']
tags: ["v*.*.*"]

env:
REGISTRY: ghcr.io
Expand All @@ -18,21 +18,21 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
Expand All @@ -44,7 +44,7 @@ jobs:
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
push: true
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.13'
cache: 'pip'
python-version: "3.13"
cache: "pip"
cache-dependency-path: backend/pyproject.toml

- name: Install dependencies
Expand All @@ -39,13 +39,13 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
node-version: "24"
cache: "npm"
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
Expand Down
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,42 @@ All notable changes to this project will be documented in this file.

All changes were made in collaboration with [Claude](https://claude.ai) (Anthropic).

## [3.1.0]

### ⚠ Action Required

- On first launch after upgrading, you'll be asked to create an admin username and password before the dashboard becomes accessible. This is a one-time setup — every request after that requires logging in.
- If you run this app over plain HTTP without a TLS reverse proxy in front of it (e.g. LAN-only), set `SESSION_COOKIE_SECURE=false` in your environment, otherwise the login cookie won't be accepted by the browser and you won't be able to log in.

### Security

- Added a login gate: a single admin account (created via an in-app setup wizard) is now required to use the dashboard or its API. Sessions are server-side (stored in the database, survive a restart), identified by a random token — only its hash is persisted, never the raw value
- Login lockout after 5 failed attempts (15 minutes)
- Every API router and the `/ws/events` WebSocket now require a valid session, not just the frontend
- Updated frontend dependencies (react-router, vite, vitest, postcss, and transitive packages) to resolve all `npm audit` findings — 14 vulnerabilities down to 0
- Bumped all GitHub Actions (checkout, setup-python, setup-node, docker/*) to their latest major versions

### Added

- Setup wizard and login page; "Change password" and "Log out" controls on the Settings page

### Performance

- Docker image shrunk from 419MB to 371MB: added a missing `.dockerignore` (was letting the build accidentally copy `frontend/node_modules` from the host into the image), dropped `curl` in favor of a Python-based healthcheck, and dropped uvicorn's unused `[standard]` extra (uvloop/httptools/watchfiles aren't needed since the container never runs with `--reload`)

### Removed

- Deleted `backend/src/db/migrations/`, an orphaned duplicate of `backend/src/alembic/` left over from before the project moved its migrations there — nothing imported it, and its revision history had already diverged from the real one
- Deleted `backend/src/db/seeds/` (an empty, unreferenced package) and `NotificationService.get_logs_count()` (unused method)
- Deleted unused frontend hooks (`useGames`, `useSystem`, `useEntry`, `useHistory`, `useGiveaway`, `useRefreshGiveawayGame`, `useEntryTrends`, `useWebSocketAnyEvent`) and their now-unused types (`SystemInfo`, `HealthCheck`, `TrendDataPoint`, `GameFilters`) — none were wired into any page
- Removed the unused `date-fns` dependency from `frontend/package.json`

### Fixed

- The Docker healthcheck and container `HEALTHCHECK` pinged `/api/v1/system/health`, which is now behind login — added an unauthenticated `/health` route so the container correctly reports "healthy" again
- `vitest.config.ts` was missing the `__APP_VERSION__` define that `vite.config.ts` already had, crashing any test that rendered the sidebar
- The sidebar was missing a link to the Settings page (the route existed but was only reachable by typing the URL directly)

## [3.0.5]

### Fixed
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Thank you for considering contributing to SteamSelfGifter!
## Development Setup

### Backend

```bash
cd backend
python -m venv .venv
Expand All @@ -23,6 +24,7 @@ pytest # Verify setup
```

### Frontend

```bash
cd frontend
npm install
Expand All @@ -46,6 +48,7 @@ npm test # Verify setup
### Commit Messages

Use clear, descriptive commit messages:

```
Add safety check toggle to settings page
Fix timezone handling in giveaway end times
Expand All @@ -55,13 +58,15 @@ Update API documentation for new endpoints
## Testing

### Backend

```bash
cd backend
pytest # All tests
pytest --cov=src # With coverage
```

### Frontend

```bash
cd frontend
npm test # All tests
Expand Down
12 changes: 9 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ FROM python:3.13-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
nginx \
supervisor \
curl \
&& rm -rf /var/lib/apt/lists/*

# Create non-root user for backend process
Expand Down Expand Up @@ -87,6 +86,13 @@ server {
try_files $uri $uri/ /index.html;
}

# Unauthenticated health check for the container HEALTHCHECK / orchestrators.
# Proxied directly to the backend (not under /api/) so it isn't gated by login.
location = /health {
proxy_pass http://127.0.0.1:8000/health;
proxy_set_header Host $host;
}

# Proxy API requests to backend
location /api/ {
proxy_pass http://127.0.0.1:8000;
Expand Down Expand Up @@ -151,9 +157,9 @@ SUPERVISOR_CONF
# Expose port 80 (nginx serves both frontend and proxies to backend)
EXPOSE 80

# Health check
# Health check (uses the venv's Python instead of installing curl)
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost/api/v1/system/health || exit 1
CMD python3 -c "import sys,urllib.request; sys.exit(0 if urllib.request.urlopen('http://localhost/health', timeout=5).status == 200 else 1)" || exit 1

# Start supervisor (manages nginx + uvicorn)
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ SteamSelfGifter is an automated bot for entering Steam game giveaways on SteamGi

## Features

- **Login Protection**: A one-time setup wizard creates an admin account; the dashboard and its API require logging in
- **Multi-Account Support**: Manage multiple SteamGifts accounts from a single dashboard, with an account switcher in the sidebar
- **Per-Account Settings**: Every setting (DLC, Safety, Auto-Join Rules, Scheduler, Rate Limiting) is configurable independently per account
- **Web Dashboard**: Modern React-based UI for monitoring and control
Expand Down Expand Up @@ -92,6 +93,12 @@ npm run dev # Development server at http://localhost:5173

## Configuration

### First run: account setup

The first time you open the web interface, you'll be asked to create an admin username and password — this is a one-time setup, after which every visit requires logging in. Sessions last up to 24 hours or until you log out.

If you run SteamSelfGifter over plain HTTP without a TLS reverse proxy in front of it (e.g. LAN-only access), set the `SESSION_COOKIE_SECURE=false` environment variable, otherwise the browser will refuse the login cookie and you won't be able to log in.

Settings are configured **per account** via the Accounts page:

1. Open the web interface
Expand All @@ -111,7 +118,7 @@ Settings are configured **per account** via the Accounts page:
> Each account after the first receives a **5-minute scan start offset** to prevent simultaneous requests from the same IP. With a default scan interval of 30 minutes this works fine for up to 6 accounts. For more accounts, or if you use a shorter scan interval, you must increase the **Scan Interval** setting accordingly:
>
> | Accounts | Required scan interval |
> |----------|------------------------|
> | -------- | ---------------------- |
> | 1 | any |
> | 2 | > 5 min |
> | 3 | > 10 min |
Expand Down Expand Up @@ -160,10 +167,12 @@ SteamSelfGifter/
## API Documentation

Once the backend is running, visit:

- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc

When running via Docker, the API is available at:

- http://localhost:8080/api/v1/

## Development
Expand Down
1 change: 1 addition & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ uvicorn api.main:app --reload --port 8000
```

API available at:

- REST API: http://localhost:8000
- Swagger UI: http://localhost:8000/docs
- WebSocket: ws://localhost:8000/ws/events
Expand Down
6 changes: 3 additions & 3 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ packages = [
"api.schemas",
"core",
"db",
"db.migrations",
"db.seeds",
"models",
"repositories",
"services",
Expand All @@ -29,8 +27,9 @@ license = "MIT"

dependencies = [
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"uvicorn>=0.24.0",
"sqlalchemy>=2.0.0",
"greenlet>=3.0.0",
"alembic>=1.12.0",
"aiosqlite>=0.19.0",
"pydantic>=2.5.0",
Expand All @@ -42,6 +41,7 @@ dependencies = [
"structlog>=23.2.0",
"python-multipart>=0.0.6",
"websockets>=12.0",
"bcrypt>=4.1.0",
]

[project.optional-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion backend/src/alembic/README
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Generic single-database configuration with an async dbapi.
Generic single-database configuration with an async dbapi.
3 changes: 3 additions & 0 deletions backend/src/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
from models.game import Game # noqa: F401
from models.scheduler_state import SchedulerState # noqa: F401
from models.activity_log import ActivityLog # noqa: F401
from models.account import Account # noqa: F401
from models.user import User # noqa: F401
from models.session import AuthSession # noqa: F401
from core.config import settings as app_settings

# this is the Alembic Config object, which provides
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Add users and sessions tables for app login

Revision ID: b2c3d4e5f6a7
Revises: a1b2c3d4e5f6
Create Date: 2026-07-03 00:00:00.000000

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


revision: str = 'b2c3d4e5f6a7'
down_revision: Union[str, Sequence[str], None] = 'a1b2c3d4e5f6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table(
'users',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.Column('password_hash', sa.String(), nullable=False),
sa.Column('failed_attempts', sa.Integer(), nullable=False, server_default='0'),
sa.Column('locked_until', sa.DateTime(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username'),
)

op.create_table(
'sessions',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('token_hash', sa.String(), nullable=False),
sa.Column('expires_at', sa.DateTime(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('token_hash'),
)
op.create_index('ix_sessions_user_id', 'sessions', ['user_id'], unique=False)


def downgrade() -> None:
op.drop_index('ix_sessions_user_id', table_name='sessions')
op.drop_table('sessions')
op.drop_table('users')
Loading