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
23 changes: 23 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,26 @@ jobs:
- name: Tear down services
if: always()
run: docker compose -f docker-compose.dev.yml --env-file .env.ci down -v

frontend-build:
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Checkout code
uses: actions/checkout@v6

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

- name: Install dependencies
run: npm ci
working-directory: frontend

- name: Build frontend
run: npm run build
working-directory: frontend
4 changes: 4 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ async def lifespan(app: FastAPI):
allow_headers=["*"],
)

# Include routers
from app.routers import settings as settings_router
app.include_router(settings_router.router)

@app.get("/api/health")
async def health():
"""Health check endpoint with database connectivity test"""
Expand Down
Empty file added app/routers/__init__.py
Empty file.
40 changes: 40 additions & 0 deletions app/routers/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from fastapi import APIRouter
from pydantic import BaseModel
from typing import Optional
import logging

router = APIRouter(prefix="/api/settings", tags=["settings"])
logger = logging.getLogger(__name__)

class SettingsResponse(BaseModel):
ai_context: Optional[str] = None
auto_sync: bool = True
usage_analytics: bool = True


class SettingsUpdate(BaseModel):
ai_context: Optional[str] = None
auto_sync: Optional[bool] = None
usage_analytics: Optional[bool] = None


@router.get("", response_model=SettingsResponse)
async def get_settings():
logger.debug("Fetching user settings - return a stub")
"""Fetch user settings. Returns a stub for now."""
return SettingsResponse(
ai_context="",
auto_sync=True,
usage_analytics=True
)


@router.patch("", response_model=SettingsResponse)
async def update_settings(settings: SettingsUpdate):
"""Update user settings. Does nothing for now, just returns the current stub."""
logger.debug("Updating user settings with request %s" % str(settings))
return SettingsResponse(
Comment on lines +33 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Settings payload logged verbatim 📘 Rule violation ⛨ Security

• The backend logs the full SettingsUpdate request object, which may contain sensitive or
  user-provided free-text in ai_context.
• This violates secure logging expectations because logs may capture PII or sensitive preferences
  and are not structured/redacted.
• Similar patterns on the frontend log raw error objects, which can include server response
  details and request context.
Agent Prompt
## Issue description
Current logging includes full settings payloads (including `ai_context`) and raw `error` objects. This can leak sensitive user data into logs and produces unstructured logs.

## Issue Context
`ai_context` is user-provided free text and may contain PII. Error objects may contain response bodies, stack traces, or other sensitive details depending on the API client.

## Fix Focus Areas
- app/routers/settings.py[33-36]
- frontend/src/components/SettingsModal.tsx[48-51]
- frontend/src/lib/auth.ts[121-124]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +32 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. No user audit context 📘 Rule violation ✧ Quality

• The settings update action is not recorded as an audit event with required context (user ID,
  action description, and outcome).
• The current log line is a debug message without a user identifier and does not clearly record
  success/failure, making event reconstruction difficult.
• This weakens traceability for sensitive preference changes and hinders security investigations.
Agent Prompt
## Issue description
Settings updates are not captured in a compliant audit trail (missing user ID and outcome). Existing debug logs also risk capturing sensitive payloads.

## Issue Context
Audit trails should allow reconstruction of who changed what and whether it succeeded, while avoiding sensitive content in log bodies.

## Fix Focus Areas
- app/routers/settings.py[32-36]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

ai_context=settings.ai_context or "",
auto_sync=settings.auto_sync if settings.auto_sync is not None else True,
usage_analytics=settings.usage_analytics if settings.usage_analytics is not None else True
)
Comment on lines +21 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Settings endpoints missing auth 📘 Rule violation ⛨ Security

• The new /api/settings read/write endpoints (get_settings, update_settings) do not
  authenticate the caller or authorize access to a specific user.
• This allows any client that can reach the API to read or modify settings, which is improper
  handling of external input and sensitive user state.
• Settings are typically user-scoped; without a user context, the service cannot enforce per-user
  access control.
Agent Prompt
## Issue description
`/api/settings` endpoints currently accept requests without authenticating the caller or authorizing access to a specific user, which can allow unauthorized read/write of settings.

## Issue Context
Settings are user-scoped data. The API should derive a `user_id` (or equivalent) from an auth mechanism (session/JWT) and ensure reads/writes are performed only for that user.

## Fix Focus Areas
- app/routers/settings.py[21-40]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

112 changes: 108 additions & 4 deletions frontend/css/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,118 @@ body::before {
gap: 0.75rem;
}

.profile-picture {
width: 2.5rem;
height: 2.5rem;
/* User Menu */
.user-menu {
position: relative;
}

.user-menu-wrapper {
position: relative;
}

.user-menu-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem;
border: none;
background: none;
cursor: pointer;
border-radius: var(--radius-lg);
transition: background-color var(--transition-fast);
}

.user-menu-button:hover {
background-color: var(--color-surface-highlight-alpha-30);
}

.user-avatar {
width: 2rem;
height: 2rem;
border-radius: var(--radius-full);
border: 2px solid var(--color-primary);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-color: var(--color-surface-highlight);
flex-shrink: 0;
}

.user-avatar-initials {
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-text-secondary);
background: var(--color-surface-highlight);
}

.user-demo-badge {
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-text-secondary);
padding: 0.125rem 0.5rem;
border-radius: var(--radius-md);
background-color: var(--color-warning-alpha-20);
}

.user-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
min-width: 200px;
background-color: var(--color-bg-dark);
border-radius: var(--radius-lg);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid var(--color-border);
padding: 0.5rem;
z-index: 1000;
}

.user-dropdown-header {
padding: 0.75rem;
}

.user-dropdown-name {
font-size: var(--text-base);
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 0.25rem;
}

.user-dropdown-email {
font-size: var(--text-sm);
color: var(--color-text-secondary);
}

.user-dropdown-divider {
height: 1px;
background-color: var(--color-border);
margin: 0.5rem 0;
}

.user-dropdown-item {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem;
border: none;
background: none;
cursor: pointer;
border-radius: var(--radius-md);
font-size: var(--text-base);
color: var(--color-text-primary);
transition: background-color var(--transition-fast);
text-align: left;
}

.user-dropdown-item:hover {
background-color: var(--color-surface-highlight);
}

.user-dropdown-item .material-symbols-outlined {
color: var(--color-text-secondary);
}

/* ============================================
Expand Down
13 changes: 13 additions & 0 deletions frontend/css/components/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@
border-color: var(--color-primary);
}

/* Textarea Small - for modals and compact layouts */
.textarea-sm {
min-height: 120px;
padding: 1rem;
font-size: var(--text-base);
font-weight: 400;
line-height: 1.5;
}

.textarea-sm:focus {
border-color: var(--color-border-hover);
}

/* Text Input */
.form-vertical .form-group input {
width: 100%;
Expand Down
10 changes: 10 additions & 0 deletions frontend/css/components/modals.css
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@
background-color: var(--color-surface-highlight);
}

/* Ensure elements remain visible when row is hovered */
.setting-row:hover .textarea {
background-color: var(--color-surface-dark);
}

.setting-row:hover .btn {
background-color: var(--color-surface-dark);
box-shadow: 0 0 0 1px var(--color-border);
}

.setting-label {
display: flex;
align-items: center;
Expand Down
96 changes: 96 additions & 0 deletions frontend/css/components/toast.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* Toast Notifications */
.toast-container {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 99999;
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 400px;
pointer-events: none;
}

.toast {
pointer-events: auto;
}

.toast {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: slideIn 0.3s ease-out;
}

@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}

.toast-icon {
font-size: 1.25rem;
flex-shrink: 0;
}

.toast-success {
border-left: 4px solid var(--color-status-green);
}

.toast-success .toast-icon {
color: var(--color-status-green);
}

.toast-error {
border-left: 4px solid var(--color-status-red);
}

.toast-error .toast-icon {
color: var(--color-status-red);
}

.toast-info {
border-left: 4px solid var(--color-primary);
}

.toast-info .toast-icon {
color: var(--color-primary);
}

.toast-message {
flex: 1;
font-size: 0.875rem;
line-height: 1.4;
color: var(--color-text);
}

.toast-close {
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--color-text-secondary);
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
flex-shrink: 0;
}

.toast-close:hover {
color: var(--color-text);
}

.toast-close .material-symbols-outlined {
font-size: 1.125rem;
}
Loading