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
13 changes: 7 additions & 6 deletions sparkth/api/v1/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from sparkth.lib.audit.context import AnonymousActor, UserActor
from sparkth.lib.audit.events import AuditOutcome, AuditTarget, LoginAuditEvent
from sparkth.lib.db import get_async_session
from sparkth.lib.i18n import _
from sparkth.schemas import (
GoogleAuthUrl,
ResendVerificationRequest,
Expand Down Expand Up @@ -54,23 +55,23 @@ async def register_user(
session: AsyncSession = Depends(get_async_session),
) -> User:
if not settings.REGISTRATION_ENABLED:
raise HTTPException(status_code=403, detail="Registration is currently disabled")
raise HTTPException(status_code=403, detail=_("Registration is currently disabled"))

normalized_email = user.email.strip().lower()
if not await WhitelistService.is_email_allowed(session, normalized_email):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="This email address is not authorized to register. Contact an administrator.",
detail=_("This email address is not authorized to register. Contact an administrator."),
)
result = await session.exec(select(User).where(User.username == user.username))
db_user = result.one_or_none()
if db_user:
raise HTTPException(status_code=400, detail="Username already registered")
raise HTTPException(status_code=400, detail=_("Username already registered"))

result = await session.exec(select(User).where(User.email == normalized_email))
db_user_email = result.one_or_none()
if db_user_email:
raise HTTPException(status_code=400, detail="Email already registered")
raise HTTPException(status_code=400, detail=_("Email already registered"))

hashed_password = security.get_password_hash(user.password)
db_user = User(
Expand Down Expand Up @@ -140,7 +141,7 @@ async def login_for_access_token(
)
raise HTTPException(
status_code=401,
detail="Incorrect username or password",
detail=_("Incorrect username or password"),
headers={"WWW-Authenticate": "Bearer"},
)

Expand All @@ -155,7 +156,7 @@ async def login_for_access_token(
)
raise HTTPException(
status_code=401,
detail="Incorrect username or password",
detail=_("Incorrect username or password"),
headers={"WWW-Authenticate": "Bearer"},
)

Expand Down
9 changes: 5 additions & 4 deletions sparkth/api/v1/file_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from sparkth.core.models.user import User
from sparkth.lib.auth import get_current_user
from sparkth.lib.i18n import _

router: APIRouter = APIRouter()

Expand Down Expand Up @@ -38,12 +39,12 @@ async def parse_pdf(content: bytes) -> str:
@router.post("/upload")
async def upload_text(current_user: User = Depends(get_current_user), file: UploadFile = File(...)) -> JSONResponse:
if not current_user or not current_user.id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not authenticated.")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=_("User not authenticated."))

if file.content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=400,
detail="Only .txt and .pdf files are supported",
detail=_("Only .txt and .pdf files are supported"),
)

content = await file.read()
Expand All @@ -52,7 +53,7 @@ async def upload_text(current_user: User = Depends(get_current_user), file: Uplo
if size > MAX_FILE_SIZE:
raise HTTPException(
status_code=400,
detail="File size exceeds 30MB limit",
detail=_("File size exceeds 30MB limit"),
)

if file.content_type == "text/plain":
Expand All @@ -62,7 +63,7 @@ async def upload_text(current_user: User = Depends(get_current_user), file: Uplo
text = await parse_pdf(content)

else:
raise HTTPException(status_code=400, detail="Unsupported file type")
raise HTTPException(status_code=400, detail=_("Unsupported file type"))

return JSONResponse(
{
Expand Down
7 changes: 5 additions & 2 deletions sparkth/api/v1/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sparkth.core.models.user import User
from sparkth.lib.auth import get_current_user
from sparkth.lib.db import get_async_session
from sparkth.lib.i18n import _
from sparkth.lib.llm import (
DEFAULT_MODEL,
DEFAULT_PROVIDER,
Expand Down Expand Up @@ -93,7 +94,9 @@ async def update_llm_config(
service: LLMConfigService = Depends(get_llm_service),
) -> LLMConfigResponse:
if not body.name and not body.model:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Empty body. Nothing to update.")
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=_("Empty body. Nothing to update.")
)

try:
config = await service.update(
Expand Down Expand Up @@ -173,5 +176,5 @@ async def delete_llm_config(
config_id=config_id,
)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="LLM config not found")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=_("LLM config not found"))
await session.commit()
3 changes: 2 additions & 1 deletion sparkth/api/v1/user/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from sparkth.core.models.user import User
from sparkth.lib.auth import get_current_user
from sparkth.lib.db import get_async_session
from sparkth.lib.i18n import _
from sparkth.lib.permissions import has_role
from sparkth.lib.permissions.scopes import GLOBAL
from sparkth.schemas import User as UserSchema
Expand Down Expand Up @@ -76,7 +77,7 @@ async def update_user_language(
# that dependency: an authenticated principal without a row is an
# authentication-boundary condition, not a domain error to route through the
# exception-handler registry.
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=_("User not found"))

user.language = update.language
# updated_at has a default_factory but no onupdate, so the bump is manual.
Expand Down
18 changes: 12 additions & 6 deletions sparkth/api/v1/user_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
)
from sparkth.lib.auth import get_current_user
from sparkth.lib.db import get_async_session
from sparkth.lib.i18n import _
from sparkth.lib.log import get_logger

logger = get_logger(__name__)
Expand All @@ -45,7 +46,9 @@ class UserPluginConfigRequest(BaseModel):
async def get_plugin_or_404(plugin_service: PluginService, session: AsyncSession, name: str) -> Plugin:
plugin = await plugin_service.get_by_name(session, name)
if not plugin:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Plugin '{name}' not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=_("Plugin '{name}' not found").format(name=name)
)
return plugin


Expand All @@ -55,7 +58,9 @@ def validate_plugin(plugin: Plugin) -> None:
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Plugin '{plugin.name}' is not persisted."
)
if not plugin.enabled:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"Plugin '{plugin.name}' is not enabled")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=_("Plugin '{name}' is not enabled").format(name=plugin.name)
)


@router.get("/", response_model=list[UserPluginResponse])
Expand Down Expand Up @@ -115,15 +120,16 @@ async def create_user_plugin(
) -> UserPluginResponse:
"""Create a user plugin with validated configuration."""
if not current_user.id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not authenticated.")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=_("User not authenticated."))

plugin = await get_plugin_or_404(plugin_service, session, plugin_name)
validate_plugin(plugin)

user_plugin = await plugin_service.get_user_plugin(session, current_user.id, plugin.id)
if user_plugin and len(user_plugin.config) > 0:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail=f"Plugin '{plugin_name}' is already configured"
status_code=status.HTTP_409_CONFLICT,
detail=_("Plugin '{name}' is already configured").format(name=plugin_name),
)

try:
Expand Down Expand Up @@ -189,7 +195,7 @@ async def update_user_plugin(
Enable or disable a plugin for the current user.
"""
if not current_user.id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not authenticated.")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=_("User not authenticated."))

plugin = await get_plugin_or_404(plugin_service, session, plugin_name)
validate_plugin(plugin)
Expand Down Expand Up @@ -219,7 +225,7 @@ async def update_user_plugin_config(
This allows users to customize plugin-specific settings.
"""
if not current_user.id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not authenticated.")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=_("User not authenticated."))

plugin = await get_plugin_or_404(plugin_service, session, plugin_name)
validate_plugin(plugin)
Expand Down
5 changes: 3 additions & 2 deletions sparkth/core/google_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import aiohttp

from sparkth.core.config import get_settings
from sparkth.lib.i18n import _

# Google OAuth endpoints
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
Expand Down Expand Up @@ -37,7 +38,7 @@ def generate_google_login_url() -> str:
Returns:
The authorization URL to redirect the user to.
"""
client_id, _, redirect_uri = get_google_credentials()
client_id, _client_secret, redirect_uri = get_google_credentials()

params = {
"client_id": client_id,
Expand Down Expand Up @@ -100,6 +101,6 @@ async def get_google_user_info(access_token: str) -> dict[str, Any]:
headers = {"Authorization": f"Bearer {access_token}"}
async with session.get(GOOGLE_USERINFO_URL, headers=headers) as response:
if response.status != 200:
raise ValueError("Failed to get user info from Google")
raise ValueError(_("Failed to get user info from Google"))
result: dict[str, Any] = await response.json()
return result
3 changes: 2 additions & 1 deletion sparkth/core/permissions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from sparkth.lib.auth import get_current_user
from sparkth.lib.db import get_async_session
from sparkth.lib.hooks import SingleNamedItemHook
from sparkth.lib.i18n import _
from sparkth.lib.log import get_logger

logger = get_logger(__name__)
Expand Down Expand Up @@ -89,7 +90,7 @@ async def dependency(
raise RuntimeError("Permission scope is misconfigured")
scope_object_id = request.path_params.get(scope_param) if scope_param else None
if not await can(current_user, self, permission_scope, scope_object_id, session):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=_("Permission denied"))
return current_user

return dependency
Expand Down
27 changes: 18 additions & 9 deletions sparkth/core/permissions/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,69 @@
from sparkth.lib.i18n import _


class RoleNotFound(Exception):
"""Raised when a role referenced by name does not exist."""

def __init__(self, role_name: str) -> None:
super().__init__(f"Role not found: {role_name}")
super().__init__(_("Role not found: {role_name}").format(role_name=role_name))
self.role_name = role_name


class PermissionNotFound(Exception):
"""Raised when a permission referenced by name is not registered."""

def __init__(self, permission: str) -> None:
super().__init__(f"Permission not found: {permission}")
super().__init__(_("Permission not found: {permission}").format(permission=permission))
self.permission = permission


class PermissionScopeNotFound(Exception):
"""Raised when a permission scope referenced by name is not registered."""

def __init__(self, name: str) -> None:
super().__init__(f"Permission scope not found: {name}")
super().__init__(_("Permission scope not found: {name}").format(name=name))
self.name = name


class RoleAlreadyExists(Exception):
"""Raised when creating or renaming a role to a name that is already taken."""

def __init__(self, name: str) -> None:
super().__init__(f"Role already exists: {name}")
super().__init__(_("Role already exists: {name}").format(name=name))
self.name = name


class RoleInUse(Exception):
"""Raised when deleting a role that still has active assignments."""

def __init__(self, role_id: int) -> None:
super().__init__(f"Role is still assigned and cannot be deleted: {role_id}")
super().__init__(_("Role is still assigned and cannot be deleted: {role_id}").format(role_id=role_id))
self.role_id = role_id


class GroupNotFound(Exception):
"""Raised when a group referenced by id or name does not exist."""

def __init__(self, name: str) -> None:
super().__init__(f"Group not found: {name}")
super().__init__(_("Group not found: {name}").format(name=name))
self.name = name


class GroupAlreadyExists(Exception):
"""Raised when creating or renaming a group to a name that is already taken."""

def __init__(self, name: str) -> None:
super().__init__(f"Group already exists: {name}")
super().__init__(_("Group already exists: {name}").format(name=name))
self.name = name


class GroupInUse(Exception):
"""Raised when deleting a group that still has active role assignments."""

def __init__(self, group_id: int) -> None:
super().__init__(f"Group still has active role assignments and cannot be deleted: {group_id}")
super().__init__(
_("Group still has active role assignments and cannot be deleted: {group_id}").format(group_id=group_id)
)
self.group_id = group_id


Expand All @@ -72,6 +77,10 @@ class InvalidScopeObjectId(Exception):
"""

def __init__(self, scope: str, scope_object_id: str | None) -> None:
super().__init__(f"Invalid scope/object-id pairing for scope {scope!r}: {scope_object_id!r}")
super().__init__(
_("Invalid scope/object-id pairing for scope {scope!r}: {scope_object_id!r}").format(
scope=scope, scope_object_id=scope_object_id
)
)
self.scope = scope
self.scope_object_id = scope_object_id
7 changes: 5 additions & 2 deletions sparkth/core/plugins/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from sparkth.core.routes import get_route_plugin_name
from sparkth.lib.auth import decode_token_username, get_user_by_username
from sparkth.lib.db import session_scope
from sparkth.lib.i18n import _
from sparkth.lib.log import get_logger

logger = get_logger(__name__)
Expand Down Expand Up @@ -96,8 +97,10 @@ async def dispatch(self, request: Request, call_next: Callable[[Request], Awaita
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={
"detail": f"Access to plugin '{plugin_name}' is disabled for your account. "
f"Please enable the plugin in your settings."
"detail": _(
"Access to plugin '{name}' is disabled for your account. "
"Please enable the plugin in your settings."
).format(name=plugin_name)
},
)

Expand Down
Loading
Loading