Skip to content

Commit 91d73e7

Browse files
feat: Add user registration endpoints in API v2. (#10430)
* feat: Add user registration endpoints in API v2. Desktop Model-credits initiative. * [autofix.ci] apply automated fixes * fix: adjust registration API endpoints to REST compliance. * [autofix.ci] apply automated fixes * feat: Implement registration API queries for fetching and posting registrations * feat: Enhance registration API with secure file handling and add hooks for fetching registration * refactor: Lazy file creation. * Commit: Delete unused file use-get-registratrions.ts * feat: Add unit and integration tests for registration API * refactor: Simplify registration response model by removing success and message fields * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * refactor: Remove success and message assertions from registration tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 1a34d84 commit 91d73e7

8 files changed

Lines changed: 557 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,6 @@ CLAUDE.md
285285
member_servers.json
286286
# Component index cache (user-specific)
287287
**/.cache/lfx/
288+
289+
# data files used for desktop registration
290+
data/user

src/backend/base/langflow/api/router.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from langflow.api.v1.voice_mode import router as voice_mode_router
2525
from langflow.api.v2 import files_router as files_router_v2
2626
from langflow.api.v2 import mcp_router as mcp_router_v2
27+
from langflow.api.v2 import registration_router as registration_router_v2
2728

2829
router_v1 = APIRouter(
2930
prefix="/v1",
@@ -55,6 +56,7 @@
5556

5657
router_v2.include_router(files_router_v2)
5758
router_v2.include_router(mcp_router_v2)
59+
router_v2.include_router(registration_router_v2)
5860

5961
router = APIRouter(
6062
prefix="/api",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from langflow.api.v2.files import router as files_router
22
from langflow.api.v2.mcp import router as mcp_router
3+
from langflow.api.v2.registration import router as registration_router
34

45
__all__ = [
56
"files_router",
67
"mcp_router",
8+
"registration_router",
79
]
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import json
2+
from asyncio import to_thread
3+
from datetime import datetime, timezone
4+
from pathlib import Path
5+
6+
from fastapi import APIRouter, HTTPException
7+
from pydantic import BaseModel, EmailStr
8+
9+
from langflow.logging import logger
10+
11+
router = APIRouter(tags=["Registration API"], prefix="/registration")
12+
13+
14+
# Data model for registration
15+
class RegisterRequest(BaseModel):
16+
email: EmailStr
17+
18+
19+
class RegisterResponse(BaseModel):
20+
email: str
21+
22+
23+
# File to store registrations
24+
REGISTRATION_FILE = Path("data/user/registration.json")
25+
26+
27+
def _ensure_registration_file():
28+
"""Ensure registration file and directory exist with proper permissions."""
29+
try:
30+
# Ensure the directory exists with secure permissions
31+
REGISTRATION_FILE.parent.mkdir(parents=True, exist_ok=True)
32+
# Set directory permissions to owner read/write/execute only (if possible)
33+
REGISTRATION_FILE.parent.chmod(0o700)
34+
except Exception as e:
35+
logger.error(f"Failed to create registration file/directory: {e}")
36+
raise
37+
38+
39+
# TODO: Move functions to a separate service module
40+
41+
42+
def load_registration() -> dict | None:
43+
"""Load the single registration from file."""
44+
if not REGISTRATION_FILE.exists() or REGISTRATION_FILE.stat().st_size == 0:
45+
return None
46+
try:
47+
with REGISTRATION_FILE.open("rb") as f: # using binary mode for faster file IO
48+
content = f.read()
49+
return json.loads(content)
50+
except (json.JSONDecodeError, UnicodeDecodeError):
51+
logger.error(f"Corrupted registration file: {REGISTRATION_FILE}")
52+
return None
53+
54+
55+
def save_registration(email: str) -> bool:
56+
"""Save the single registration to file.
57+
58+
Args:
59+
email: Email to register
60+
61+
Returns:
62+
True if saved successfully
63+
"""
64+
try:
65+
# Ensure the registration file and directory exist
66+
_ensure_registration_file()
67+
68+
# Check if registration already exists
69+
existing = load_registration()
70+
71+
# Create new registration (replaces any existing)
72+
registration = {
73+
"email": email,
74+
"registered_at": datetime.now(tz=timezone.utc).isoformat(),
75+
}
76+
77+
# Log if replacing
78+
if existing:
79+
logger.info(f"Replacing registration: {existing.get('email')} -> {email}")
80+
81+
# Save to file
82+
with REGISTRATION_FILE.open("w") as f:
83+
json.dump(registration, f, indent=2)
84+
85+
logger.info(f"Registration saved: {email}")
86+
87+
except Exception as e:
88+
logger.error(f"Error saving registration: {e}")
89+
raise
90+
else:
91+
return True
92+
93+
94+
@router.post("/", response_model=RegisterResponse)
95+
async def register_user(request: RegisterRequest):
96+
"""Register the single user with email.
97+
98+
Note: Only one registration is allowed.
99+
"""
100+
try:
101+
email = request.email
102+
# Save to local file (replace existing) not dealing with 201 status for simplicity.
103+
if await to_thread(save_registration, email):
104+
return RegisterResponse(email=email)
105+
106+
except HTTPException:
107+
raise
108+
except Exception as e:
109+
raise HTTPException(status_code=500, detail=f"Registration failed: {e!s}") from e
110+
111+
112+
@router.get("/")
113+
async def get_registration():
114+
"""Get the registered user (if any)."""
115+
try:
116+
registration = await to_thread(load_registration)
117+
if registration:
118+
return registration
119+
120+
return {"message": "No user registered"} # noqa: TRY300
121+
122+
except Exception as e:
123+
raise HTTPException(status_code=500, detail=f"Failed to load registration: {e!s}") from e

0 commit comments

Comments
 (0)