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
18 changes: 18 additions & 0 deletions backend/app/api/docs/auth/magic_link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Request Magic Link Login

Send a magic link login email to the user's email address.

## Request Body

- **email** (required): The user's email address.

## Behavior

1. Checks if the user exists — returns 404 if not.
2. Generates a short-lived login token (15 minutes).
3. Sends an email with a "Sign In Now" button linking to the frontend.

## Error Responses

- **404**: No account found for this email.
- **500**: Email service is not configured or failed to send.
20 changes: 20 additions & 0 deletions backend/app/api/docs/auth/magic_link_verify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Verify Magic Link

Verify a magic link login token and log the user in.

## Query Parameters

- **token** (required): The login JWT token from the email link.

## Behavior

1. Validates the magic link token (checks signature, expiry, and type).
2. Looks up the user by the email embedded in the token.
3. Verifies the user is active.
4. If the user has exactly one project, it is auto-selected and embedded in the JWT.
5. Returns a JWT access token and sets HTTP-only cookies.

## Error Responses

- **400**: Invalid or expired login link.
- **404**: User account not found.
7 changes: 7 additions & 0 deletions backend/app/api/docs/credentials/delete_all_by_org_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Delete all credentials for a specific organization and project.

Permanently removes all provider credentials associated with the specified organization and project IDs. Requires superuser access.

### Path Parameters:
- **org_id**: Organization ID
- **project_id**: Project ID
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Delete credentials for a specific provider within an organization and project.

Permanently removes credentials for a specific provider from the specified organization and project. Requires superuser access.

### Path Parameters:
- **org_id**: Organization ID
- **project_id**: Project ID
- **provider**: Provider name (e.g., `openai`, `langfuse`, `google`, `sarvamai`, `elevenlabs`)
2 changes: 1 addition & 1 deletion backend/app/api/docs/credentials/get_provider.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Get credentials for a specific provider.

Retrieves decrypted credentials for a specific provider (e.g., `openai`, `langfuse`) for the current organization and project.
Retrieves credentials for a specific provider (e.g., `openai`, `langfuse`) for the current organization and project. Sensitive fields (e.g., `api_key`, `secret_key`) are masked in the response. If credentials for the provider are not configured, `null` is returned.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Get credentials for a specific provider within an organization and project.

Retrieves credentials for a specific provider (e.g., `openai`, `langfuse`) for the specified organization and project. Sensitive fields (e.g., `api_key`, `secret_key`) are masked in the response. If credentials for the provider are not configured, `null` is returned. Requires superuser access.

### Path Parameters:
- **org_id**: Organization ID
- **project_id**: Project ID
- **provider**: Provider name (e.g., `openai`, `langfuse`, `google`, `sarvamai`, `elevenlabs`)
2 changes: 1 addition & 1 deletion backend/app/api/docs/credentials/list.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Get all credentials for current organization and project.

Returns list of all provider credentials associated with your organization and project.
Returns a list of all provider credentials associated with your organization and project. Sensitive fields (e.g., `api_key`, `secret_key`) are masked in the response. If no credentials are configured, an empty list is returned.
12 changes: 12 additions & 0 deletions backend/app/api/docs/credentials/list_by_org_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Get all credentials for a specific organization and project.

Retrieves all provider credentials associated with the specified organization and project IDs. Sensitive fields (e.g., `api_key`, `secret_key`) are masked in the response. If no credentials are configured, an empty list is returned. Requires superuser access.

### Path Parameters:
- **org_id**: Organization ID
- **project_id**: Project ID

### Supported Providers:
- **LLM:** openai, sarvamai, google(gemini)
- **Observability:** langfuse
- **Audio:** elevenlabs
33 changes: 32 additions & 1 deletion backend/app/api/docs/credentials/update.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
Update credentials for a specific provider.

Updates existing provider credentials for the current organization and project. Provider and credential fields must be provided.
Updates existing provider credentials for the current organization and project. If the credentials for the specified provider don't exist yet, they will be **created** automatically (upsert behavior). The `provider` and `credential` fields are required.

The `credential` field accepts **two formats** (both work the same):

### Nested format (same as create endpoint):
```json
{
"provider": "openai",
"is_active": true,
"credential": {
"openai": {
"api_key": "sk-proj-..."
}
}
}
```

### Flat format:
```json
{
"provider": "openai",
"is_active": true,
"credential": {
"api_key": "sk-proj-..."
}
}
```

### Supported Providers:
- **LLM:** openai, sarvamai, google(gemini)
- **Observability:** langfuse
- **Audio:** elevenlabs
38 changes: 38 additions & 0 deletions backend/app/api/docs/credentials/update_by_org_project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Update credentials for a specific provider within an organization and project.

Updates existing provider credentials for the specified organization and project. If the credentials for the specified provider don't exist yet, they will be **created** automatically (upsert behavior). Requires superuser access.

### Path Parameters:
- **org_id**: Organization ID
- **project_id**: Project ID

The `credential` field accepts **two formats** (both work the same):

### Nested format (same as create endpoint):
```json
{
"provider": "openai",
"is_active": true,
"credential": {
"openai": {
"api_key": "sk-proj-..."
}
}
}
```

### Flat format:
```json
{
"provider": "openai",
"is_active": true,
"credential": {
"api_key": "sk-proj-..."
}
}
```

### Supported Providers:
- **LLM:** openai, sarvamai, google(gemini)
- **Observability:** langfuse
- **Audio:** elevenlabs
118 changes: 117 additions & 1 deletion backend/app/api/routes/auth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Any

from fastapi import APIRouter, HTTPException, Request, status
from fastapi.responses import JSONResponse
Expand All @@ -14,6 +15,7 @@
from app.models import (
GoogleAuthRequest,
GoogleAuthResponse,
MagicLinkRequest,
Message,
SelectProjectRequest,
Token,
Expand All @@ -22,10 +24,17 @@
build_google_auth_response,
build_token_response,
clear_auth_cookies,
generate_magic_link_token,
validate_refresh_token,
verify_invite_token,
verify_magic_link_token,
)
from app.utils import (
APIResponse,
generate_magic_link_email,
load_description,
send_email,
)
from app.utils import APIResponse, load_description

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -250,3 +259,110 @@ def verify_invitation(session: SessionDep, token: str) -> JSONResponse:
f"[verify_invitation] Invitation verified | user_id: {user.id}, project_id: {invite_payload.project_id}"
)
return response


@router.post(
"/magic-link",
description=load_description("auth/magic_link.md"),
response_model=APIResponse[Message],
)
def request_magic_link(session: SessionDep, body: MagicLinkRequest) -> Any:
"""Send a magic link login email to the user."""

user = get_user_by_email(session=session, email=body.email)
if not user:
logger.info(
f"[request_magic_link] Magic link requested for non-existent email: {body.email}"
)
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No account found for this email.",
)

token = generate_magic_link_token(email=body.email)

if settings.emails_enabled:
try:
email_data = generate_magic_link_email(
email_to=body.email,
magic_link_token=token,
)
send_email(
email_to=body.email,
subject=email_data.subject,
html_content=email_data.html_content,
)
logger.info(
f"[request_magic_link] Magic link email sent | email: {body.email}"
)
except Exception as e:
logger.error(
f"[request_magic_link] Failed to send magic link email | email: {body.email}, error: {e}"
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to send login email. Please try again later.",
)
else:
logger.warning("[request_magic_link] Email sending is not configured")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Email service is not configured",
)

return APIResponse.success_response(
data=Message(message="If an account exists, a login link has been sent.")
)


@router.get(
"/magic-link/verify",
description=load_description("auth/magic_link_verify.md"),
response_model=APIResponse[Token],
)
def verify_magic_link(session: SessionDep, token: str) -> JSONResponse:
"""Verify a magic link token and log the user in."""

email = verify_magic_link_token(token)
if not email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid or expired login link. Please request a new one.",
)

user = get_user_by_email(session=session, email=email)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User account not found",
)

# Activate user if not already active
if not user.is_active:
user.is_active = True
session.add(user)
session.commit()
session.refresh(user)
logger.info(
f"[verify_magic_link] User activated via magic link | user_id: {user.id}"
)

# Get user's projects to embed in token
available_projects = get_user_accessible_projects(session=session, user_id=user.id)

organization_id = None
project_id = None
if len(available_projects) == 1:
organization_id = available_projects[0]["organization_id"]
project_id = available_projects[0]["project_id"]

response = build_token_response(
user_id=user.id,
organization_id=organization_id,
project_id=project_id,
)

logger.info(
f"[verify_magic_link] User logged in via magic link | user_id: {user.id}"
)
return response
Loading