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
41 changes: 14 additions & 27 deletions backend/api/auth/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,25 @@
from core.config import settings


class UserResponse(BaseModel):
id: str = Field(..., description="User ID")
first_name: str = Field(..., description="First name")
last_name: str = Field(..., description="Last name")
email: str = Field(..., description="User email address")
phone: str = Field(..., description="Phone number")


class LoginResult(TypedDict):
user: UserResponse
session_id: str = Field(..., description="Session ID")
access_token: str = Field(..., description="JWT access token")
csrf_token: str = Field(..., description="CSRF token")
session_id: str
access_token: str
csrf_token: str


class SessionResult(TypedDict):
session_id: str = Field(..., description="Session ID")
access_token: str = Field(..., description="JWT access token")
csrf_token: str = Field(..., description="CSRF token")
session_id: str
access_token: str
csrf_token: str


class UserRegister(BaseModel):
Expand All @@ -34,14 +42,6 @@ class UserLogin(BaseModel):
password: str = Field(..., min_length=1, description="Password")


class UserResponse(BaseModel):
id: str = Field(..., description="User ID")
first_name: str = Field(..., description="First name")
last_name: str = Field(..., description="Last name")
email: str = Field(..., description="User email address")
phone: str = Field(..., description="Phone number")


class UserLoginResponse(BaseModel):
access_token: str = Field(..., description="JWT access token")
expires_at: datetime = Field(..., description="Token expiration time")
Expand Down Expand Up @@ -89,19 +89,6 @@ class PasswordResetCooldownResponse(BaseModel):
cooldown_seconds: int = Field(..., description="Remaining cooldown time in seconds")


class EmailVerificationResponse(BaseModel):
message: str = Field(..., description="Verification result message")


class EmailVerificationRequiredResponse(BaseModel):
expires_at: str | None = Field(default=None, description="Token expiration time (ISO format)")


class PasswordResetRequiredResponse(BaseModel):
reset_token: str = Field(..., description="Password reset token")
expires_at: str = Field(..., description="Token expiration time (ISO format)")


class ResendVerificationRequest(BaseModel):
email: EmailStr = Field(..., description="Email address to resend verification")

Expand Down
15 changes: 5 additions & 10 deletions backend/core/rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ async def get_user_role_level(user_id: str, db: AsyncSession) -> int:
level = result.scalar_one_or_none()
return int(level) if level is not None else 0
except Exception as e:
logger.error(f"Failed to get user role level: {e}")
return 0
raise ServerException(f"Failed to get user role level: {e}")


async def get_user_role_id(user_id: str, db: AsyncSession) -> str | None:
Expand All @@ -47,8 +46,7 @@ async def get_user_role_id(user_id: str, db: AsyncSession) -> str | None:
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Failed to get user role id: {e}")
return None
raise ServerException(f"Failed to get user role id: {e}")


async def user_has_role(user_id: str, role_id: str, db: AsyncSession) -> bool:
Expand All @@ -62,8 +60,7 @@ async def user_has_role(user_id: str, role_id: str, db: AsyncSession) -> bool:
)
return result.scalar_one_or_none() is not None
except Exception as e:
logger.error(f"Failed to check user role assignment: {e}")
return False
raise ServerException(f"Failed to check user role assignment: {e}")


async def get_user_attributes(user_id: str, db: AsyncSession) -> dict[str, bool]:
Expand Down Expand Up @@ -95,8 +92,7 @@ async def get_user_attributes(user_id: str, db: AsyncSession) -> dict[str, bool]

return attributes
except Exception as e:
ServerException(f"Failed to get user attributes: {e}")
return {}
raise ServerException(f"Failed to get user attributes: {e}")


async def check_user_has_super_role(user_id: str, db: AsyncSession) -> bool:
Expand All @@ -111,8 +107,7 @@ async def check_user_has_super_role(user_id: str, db: AsyncSession) -> bool:
user_roles = [row.name for row in result]
return settings.DEFAULT_SUPER_ADMIN_ROLE in user_roles
except Exception as e:
logger.error(f"Failed to check super role: {e}")
return False
raise ServerException(f"Failed to check super role: {e}")


def require_permission(required_attributes: list[str]):
Expand Down
36 changes: 7 additions & 29 deletions backend/tests/api/auth/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
LoginResult,
LogoutRequest,
PasswordResetCooldownResponse,
PasswordResetRequiredResponse,
ResetPasswordRequest,
SessionResult,
TokenResponse,
Expand Down Expand Up @@ -197,25 +196,6 @@ def test_token_response_missing_fields(self):
errors = exc_info.value.errors()
assert len(errors) == 1

def test_password_reset_required_response_valid(self):
"""Test valid password reset required response data"""
data = {"reset_token": "test-reset-token", "expires_at": "2024-01-01T00:00:00Z"}

password_reset_response = PasswordResetRequiredResponse(**data)

assert password_reset_response.reset_token == "test-reset-token"
assert password_reset_response.expires_at == "2024-01-01T00:00:00Z"

def test_password_reset_required_response_missing_fields(self):
"""Test missing required fields"""
data = {"reset_token": "test-reset-token"}

with pytest.raises(ValidationError) as exc_info:
PasswordResetRequiredResponse(**data)

errors = exc_info.value.errors()
assert len(errors) == 1

def test_logout_request_valid(self):
"""Test valid logout request data"""
data = {"logout_all": True}
Expand Down Expand Up @@ -340,33 +320,31 @@ def test_password_reset_cooldown_response_missing_field(self):

def test_login_result_typing(self):
"""Test LoginResult TypedDict"""
user_data = {
"id": "test-user-id",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+1234567890",
}
user = object()

login_result: LoginResult = {
"user": user_data,
"user": user,
"session_id": "test-session-id",
"access_token": "test-access-token",
"csrf_token": "test-csrf-token",
}

assert login_result["user"] == user_data
assert login_result["user"] is user
assert login_result["session_id"] == "test-session-id"
assert login_result["access_token"] == "test-access-token"
assert login_result["csrf_token"] == "test-csrf-token"

def test_session_result_typing(self):
"""Test SessionResult TypedDict"""
session_result: SessionResult = {
"session_id": "test-session-id",
"access_token": "test-access-token",
"csrf_token": "test-csrf-token",
}

assert session_result["session_id"] == "test-session-id"
assert session_result["access_token"] == "test-access-token"
assert session_result["csrf_token"] == "test-csrf-token"

def test_email_validation_edge_cases(self):
"""Test email validation edge cases"""
Expand Down
11 changes: 7 additions & 4 deletions backend/tests/core/test_rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from models.role_mapper import RoleMapper
from models.roles import Roles
from models.users import Users
from utils.custom_exception import ServerException


class TestIsSuperAdminRoleName:
Expand Down Expand Up @@ -54,10 +55,11 @@ async def test_returns_false_without_role(
assert await check_user_has_super_role(test_user.id, test_db_session) is False

@pytest.mark.asyncio
async def test_returns_false_on_error(self):
async def test_raises_server_exception_on_error(self):
db = AsyncMock()
db.execute.side_effect = RuntimeError("db down")
assert await check_user_has_super_role("user-1", db) is False
with pytest.raises(ServerException, match="Failed to check super role"):
await check_user_has_super_role("user-1", db)


class TestGetUserAttributes:
Expand Down Expand Up @@ -102,10 +104,11 @@ async def test_merges_attribute_values_with_or(
assert attributes["view-users"] is True

@pytest.mark.asyncio
async def test_returns_empty_on_error(self):
async def test_raises_server_exception_on_error(self):
db = AsyncMock()
db.execute.side_effect = RuntimeError("db down")
assert await get_user_attributes("user-1", db) == {}
with pytest.raises(ServerException, match="Failed to get user attributes"):
await get_user_attributes("user-1", db)


class TestRequirePermission:
Expand Down
15 changes: 11 additions & 4 deletions nginx/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,16 @@ EOF
# Start cron service
service cron start

# Auto-create whitelist.conf from example on first run (nginx/whitelist.conf on host)
if [ -f /etc/nginx/host/whitelist.conf.example ] && [ ! -f /etc/nginx/host/whitelist.conf ]; then
cp /etc/nginx/host/whitelist.conf.example /etc/nginx/host/whitelist.conf
# Auto-create whitelist.conf from example on first run (nginx/whitelist.conf on host).
# If a directory exists at that path (common Docker bind-mount mistake), replace it.
WHITELIST_PATH=/etc/nginx/host/whitelist.conf
WHITELIST_EXAMPLE=/etc/nginx/host/whitelist.conf.example
if [ -d "$WHITELIST_PATH" ]; then
echo "WARNING: $WHITELIST_PATH is a directory; replacing with file from example"
rm -rf "$WHITELIST_PATH"
fi
if [ -f "$WHITELIST_EXAMPLE" ] && [ ! -f "$WHITELIST_PATH" ]; then
cp "$WHITELIST_EXAMPLE" "$WHITELIST_PATH"
fi

# Process all template files
Expand All @@ -67,4 +74,4 @@ for template in /etc/nginx/templates/*.conf; do
done

# Start nginx
exec nginx -g 'daemon off;'
exec nginx -g 'daemon off;'
Loading