feat: implement user authentication module

- Backend (FastAPI):
  - External API authentication (pj-auth-api.vercel.app)
  - JWT token validation with Redis session storage
  - RBAC with department isolation
  - User, Role, Department models with pjctrl_ prefix
  - Alembic migrations with project-specific version table
  - Complete test coverage (13 tests)

- Frontend (React + Vite):
  - AuthContext for state management
  - Login page with error handling
  - Protected route component
  - Dashboard with user info display

- OpenSpec:
  - 7 capability specs defined
  - add-user-auth change archived

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2025-12-28 23:41:37 +08:00
commit 1fda7da2c2
77 changed files with 6562 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
import pytest
from app.core.security import create_access_token, decode_access_token, create_token_payload
class TestJWT:
"""Test JWT token creation and validation."""
def test_create_access_token(self):
"""Test creating an access token."""
data = {"sub": "user123", "email": "test@example.com"}
token = create_access_token(data)
assert token is not None
assert isinstance(token, str)
def test_decode_valid_token(self):
"""Test decoding a valid token."""
data = create_token_payload(
user_id="user123",
email="test@example.com",
role="engineer",
department_id="dept123",
is_system_admin=False,
)
token = create_access_token(data)
payload = decode_access_token(token)
assert payload is not None
assert payload["sub"] == "user123"
assert payload["email"] == "test@example.com"
assert payload["role"] == "engineer"
assert payload["is_system_admin"] is False
def test_decode_invalid_token(self):
"""Test decoding an invalid token."""
payload = decode_access_token("invalid.token.here")
assert payload is None
def test_token_payload_structure(self):
"""Test token payload has correct structure."""
payload = create_token_payload(
user_id="user123",
email="test@example.com",
role="engineer",
department_id="dept123",
is_system_admin=False,
)
assert "sub" in payload
assert "email" in payload
assert "role" in payload
assert "department_id" in payload
assert "is_system_admin" in payload
class TestAuthEndpoints:
"""Test authentication API endpoints."""
def test_get_me_without_auth(self, client):
"""Test accessing /me without authentication."""
response = client.get("/api/auth/me")
assert response.status_code == 403
def test_get_me_with_auth(self, client, admin_token):
"""Test accessing /me with valid authentication."""
response = client.get(
"/api/auth/me",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["email"] == "ymirliu@panjit.com.tw"
assert data["is_system_admin"] is True
def test_logout(self, client, admin_token, mock_redis):
"""Test logout endpoint."""
response = client.post(
"/api/auth/logout",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
# Verify session is removed
assert mock_redis.get("session:00000000-0000-0000-0000-000000000001") is None