Skip to content

Commit e7f341a

Browse files
committed
test
Signed-off-by: kerthcet <kerthcet@gmail.com>
1 parent a228ebe commit e7f341a

41 files changed

Lines changed: 2436 additions & 1353 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ ALPHATRION_CLICKHOUSE_ENABLE_BATCH=true
1717
ALPHATRION_ENABLE_PROMETHEUS=false
1818
ALPHATRION_PROMETHEUS_PUSHGATEWAY_URL=localhost:9091
1919
ALPHATRION_PROMETHEUS_JOB_NAME=alphatrion
20+
21+
# Authentication configurations
22+
# Set to true to enable JWT authentication, false to use direct headers (x-user-id, x-org-id)
23+
ALPHATRION_ENABLE_AUTH=true
24+
ALPHATRION_JWT_SECRET=your_jwt_secret_key_here

README.md

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
</p>
77

88
<h3 align="center">
9-
Open, modular framework to build and optimize GenAI applications
9+
⚒️ The observability platform for agentic systems.
1010
</h3>
1111

1212
[![stability-alpha](https://img.shields.io/badge/stability-alpha-f4d03f.svg)](https://github.com/mkenney/software-guides/blob/master/STABILITY-BADGES.md#alpha)
@@ -25,11 +25,15 @@ Open, modular framework to build and optimize GenAI applications
2525
- **🤖 Model Distribution** - Analyze request patterns and usage across different AI models
2626
- **📦 Artifact Management** - Store and version execution results, checkpoints, and model outputs
2727
- **🎯 Interactive Dashboard** - Modern web UI for exploring experiments, metrics, and traces
28+
- **🔐 Secure Authentication** - JWT-based authentication with user profiles and multi-team support
29+
- **👥 Multi-User Support** - Collaborative workspace with organization and team management
2830
- **🔌 Easy Integration** - Simple Python API with async/await support
2931

3032
## Core Concepts
3133

32-
- **Team** - Top-level organizational unit for user collaboration
34+
- **Organization** - Top-level entity for grouping teams and users
35+
- **Team** - Collaborative workspace for organizing experiments and runs
36+
- **User** - Individual account with secure authentication and team memberships
3337
- **Experiment** - Logical grouping of runs with shared purpose, organized by labels
3438
- **Run** - Individual execution instance with configuration and metrics
3539

@@ -56,12 +60,10 @@ make up
5660
# Wait for services to be ready, then run migrations
5761
make migrate
5862

59-
# Initialize your team and user
60-
alphatrion init # Use -h for custom options
63+
# Initialize your organization, team, and user account
64+
alphatrion init
6165
```
6266

63-
Save the generated user ID — you'll need it to track experiments.
64-
6567
**Optional Tools:**
6668
- pgAdmin: `http://localhost:8081` (alphatrion@inftyai.com / alphatr1on)
6769
- Registry UI: `http://localhost:80`
@@ -96,7 +98,7 @@ alphatrion server
9698
alphatrion dashboard
9799
```
98100

99-
Access the dashboard at `http://127.0.0.1:5173` to explore experiments, visualize metrics, and analyze traces.
101+
Access the dashboard at `http://127.0.0.1:5173` and **log in with your email and password** to explore experiments, visualize metrics, and analyze traces.
100102

101103
![dashboard](./site/images/dashboard.png)
102104

@@ -106,13 +108,6 @@ AlphaTrion automatically captures distributed tracing data for all LLM calls, in
106108

107109
![tracing](./site/images/trace.png)
108110

109-
### 6. Other APIs
110-
111-
- **log_params**: Track hyperparameters and configuration settings
112-
- **log_metrics**: Record performance metrics and visualize trends
113-
- **log_artifacts**: Store and manage files, checkpoints, and model outputs
114-
115-
116111
### Cleanup
117112

118113
```bash

alphatrion/envs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@
2525

2626
# Runtime related envs
2727
ROOT_PATH = "ALPHATRION_ROOT_PATH"
28+
29+
ENABLE_AUTH = "ALPHATRION_ENABLE_AUTH"
30+
JWT_SECRET = "ALPHATRION_JWT_SECRET"

alphatrion/server/auth.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Authentication utilities for JWT-based authentication."""
2+
3+
import os
4+
from datetime import UTC, datetime, timedelta
5+
from typing import Any
6+
7+
import bcrypt
8+
from jose import JWTError, jwt
9+
10+
from alphatrion import envs
11+
12+
# JWT settings
13+
SECRET_KEY = os.getenv(envs.JWT_SECRET, "your-secret-key-change-this-in-production")
14+
ALGORITHM = "HS256"
15+
ACCESS_TOKEN_EXPIRE_DAYS = 7
16+
17+
18+
def hash_password(password: str) -> str:
19+
"""Hash a password using bcrypt.
20+
21+
Args:
22+
password: Plain text password
23+
24+
Returns:
25+
Bcrypt hash of the password
26+
"""
27+
# Convert password to bytes
28+
password_bytes = password.encode("utf-8")
29+
# Generate salt and hash
30+
salt = bcrypt.gensalt()
31+
hashed = bcrypt.hashpw(password_bytes, salt)
32+
# Return as string
33+
return hashed.decode("utf-8")
34+
35+
36+
def verify_password(plain_password: str, hashed_password: str) -> bool:
37+
"""Verify a password against a bcrypt hash.
38+
39+
Args:
40+
plain_password: Plain text password to verify
41+
hashed_password: Bcrypt hash to verify against
42+
43+
Returns:
44+
True if password matches, False otherwise
45+
"""
46+
try:
47+
password_bytes = plain_password.encode("utf-8")
48+
hashed_bytes = hashed_password.encode("utf-8")
49+
return bcrypt.checkpw(password_bytes, hashed_bytes)
50+
except Exception:
51+
return False
52+
53+
54+
def create_access_token(
55+
data: dict[str, Any], expires_delta: timedelta | None = None
56+
) -> str:
57+
"""Create a JWT access token.
58+
59+
Args:
60+
data: Dictionary containing claims to encode in the token
61+
expires_delta: Optional token expiration time
62+
63+
Returns:
64+
Encoded JWT token string
65+
"""
66+
to_encode = data.copy()
67+
if expires_delta:
68+
expire = datetime.now(UTC) + expires_delta
69+
else:
70+
expire = datetime.now(UTC) + timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS)
71+
72+
to_encode.update({"exp": expire})
73+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
74+
return encoded_jwt
75+
76+
77+
def decode_access_token(token: str) -> dict[str, Any] | None:
78+
"""Decode and validate a JWT access token.
79+
80+
Args:
81+
token: JWT token string
82+
83+
Returns:
84+
Dictionary containing the decoded claims, or None if invalid
85+
"""
86+
try:
87+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
88+
return payload
89+
except JWTError:
90+
return None

alphatrion/server/cmd/app.py

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@
22
# ruff: noqa: B904
33

44
import logging
5+
import uuid
56
from importlib.metadata import version
67

7-
from fastapi import FastAPI, Request
8+
from fastapi import FastAPI, HTTPException, Request
89
from fastapi.middleware.cors import CORSMiddleware
10+
from pydantic import BaseModel
911
from strawberry.fastapi import GraphQLRouter
1012

13+
from alphatrion.server.auth import (
14+
create_access_token,
15+
decode_access_token,
16+
hash_password,
17+
verify_password,
18+
)
1119
from alphatrion.server.graphql.context import get_context
1220
from alphatrion.server.graphql.schema import schema
21+
from alphatrion.storage import runtime
1322

1423
# Configure logging
1524
logger = logging.getLogger(__name__)
@@ -117,3 +126,133 @@ def health_check():
117126
@app.get("/version")
118127
def get_version():
119128
return {"version": version("alphatrion"), "status": "ok"}
129+
130+
131+
# Auth endpoints
132+
class LoginRequest(BaseModel):
133+
email: str
134+
password: str
135+
136+
137+
class LoginResponse(BaseModel):
138+
access_token: str
139+
token_type: str
140+
user: dict
141+
142+
143+
@app.post("/api/auth/login", response_model=LoginResponse)
144+
async def login(credentials: LoginRequest):
145+
"""Authenticate user and return JWT token with user information."""
146+
try:
147+
metadb = runtime.storage_runtime().metadb
148+
149+
# Find user by email
150+
user = metadb.get_user_by_email(email=credentials.email)
151+
152+
if not user:
153+
raise HTTPException(status_code=401, detail="Invalid email or password")
154+
155+
# Verify password
156+
if not verify_password(credentials.password, user.password_hash):
157+
raise HTTPException(status_code=401, detail="Invalid email or password")
158+
159+
# Get user's teams
160+
team_members = metadb.get_team_members_by_user_id(user_id=user.uuid)
161+
teams = []
162+
for member in team_members:
163+
team = metadb.get_team(team_id=member.team_id)
164+
if team:
165+
teams.append(
166+
{
167+
"id": str(team.uuid),
168+
"name": team.name,
169+
"description": team.description,
170+
}
171+
)
172+
173+
# Create JWT token with user claims
174+
# Note: team_id is NOT included - users can belong to multiple teams
175+
# Team selection is handled in the UI
176+
token_data = {
177+
"sub": str(user.uuid), # subject = user_id
178+
"user_id": str(user.uuid),
179+
"org_id": str(user.org_id),
180+
"email": user.email,
181+
}
182+
183+
access_token = create_access_token(data=token_data)
184+
185+
# Return token and user info
186+
return {
187+
"access_token": access_token,
188+
"token_type": "bearer",
189+
"user": {
190+
"id": str(user.uuid),
191+
"name": user.name,
192+
"email": user.email,
193+
"avatarUrl": user.avatar_url,
194+
"meta": user.meta,
195+
"createdAt": user.created_at.isoformat(),
196+
"updatedAt": user.updated_at.isoformat(),
197+
"teams": teams,
198+
},
199+
}
200+
201+
except HTTPException:
202+
raise
203+
except Exception as e:
204+
logger.error(f"Login failed: {e}")
205+
raise HTTPException(status_code=500, detail="Internal server error")
206+
207+
208+
class ChangePasswordRequest(BaseModel):
209+
current_password: str
210+
new_password: str
211+
212+
213+
@app.post("/api/auth/change-password")
214+
async def change_password(request: Request, password_data: ChangePasswordRequest):
215+
"""Change user's password."""
216+
try:
217+
# Extract token from Authorization header
218+
auth_header = request.headers.get("Authorization")
219+
if not auth_header or not auth_header.startswith("Bearer "):
220+
raise HTTPException(
221+
status_code=401, detail="Missing or invalid authorization header"
222+
)
223+
224+
token = auth_header.replace("Bearer ", "")
225+
226+
# Decode token to get user_id
227+
payload = decode_access_token(token)
228+
if not payload:
229+
raise HTTPException(status_code=401, detail="Invalid or expired token")
230+
231+
user_id = payload.get("user_id")
232+
if not user_id:
233+
raise HTTPException(status_code=401, detail="Invalid token payload")
234+
235+
metadb = runtime.storage_runtime().metadb
236+
237+
# Get user from database
238+
user = metadb.get_user(user_id=uuid.UUID(user_id))
239+
if not user:
240+
raise HTTPException(status_code=404, detail="User not found")
241+
242+
# Verify current password
243+
if not verify_password(password_data.current_password, user.password_hash):
244+
raise HTTPException(status_code=401, detail="Current password is incorrect")
245+
246+
# Hash new password
247+
new_password_hash = hash_password(password_data.new_password)
248+
249+
# Update password in database
250+
metadb.update_user(user_id=uuid.UUID(user_id), password_hash=new_password_hash)
251+
252+
return {"message": "Password changed successfully"}
253+
254+
except HTTPException:
255+
raise
256+
except Exception as e:
257+
logger.error(f"Password change failed: {e}")
258+
raise HTTPException(status_code=500, detail="Internal server error")

0 commit comments

Comments
 (0)