Paramjit Singh commited on
Commit
d16cff5
·
unverified ·
2 Parent(s): 10f60a67f97cc1

Merge pull request #324 from Xenon010101/feat/api-key-management

Browse files
backend/app/auth.py CHANGED
@@ -77,18 +77,18 @@ def get_current_user(
77
  token = credentials.credentials
78
 
79
  # Check if token is an API key
80
- if token.startswith("rag_"):
81
  hashed = hashlib.sha256(token.encode("utf-8")).hexdigest()
82
  from app.models import ApiKey
83
- api_key = db.query(ApiKey).filter(ApiKey.hashed_key == hashed).first()
84
  if not api_key:
85
  raise HTTPException(
86
  status_code=status.HTTP_401_UNAUTHORIZED,
87
  detail="Invalid API key",
88
  headers={"WWW-Authenticate": "Bearer"},
89
  )
90
-
91
- api_key.last_used = datetime.now(timezone.utc)
92
  db.commit()
93
 
94
  user = api_key.user
 
77
  token = credentials.credentials
78
 
79
  # Check if token is an API key
80
+ if token.startswith("pdf_rag_"):
81
  hashed = hashlib.sha256(token.encode("utf-8")).hexdigest()
82
  from app.models import ApiKey
83
+ api_key = db.query(ApiKey).filter(ApiKey.hashed_key == hashed, ApiKey.is_active == True).first()
84
  if not api_key:
85
  raise HTTPException(
86
  status_code=status.HTTP_401_UNAUTHORIZED,
87
  detail="Invalid API key",
88
  headers={"WWW-Authenticate": "Bearer"},
89
  )
90
+
91
+ api_key.last_used_at = datetime.now(timezone.utc)
92
  db.commit()
93
 
94
  user = api_key.user
backend/app/database.py CHANGED
@@ -71,6 +71,27 @@ def _migrate_schema():
71
  "Migration skipped (may already exist): %s.%s", table, column
72
  )
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  # Migrate documents
75
  existing_docs_columns = {c["name"] for c in inspector.get_columns("documents")}
76
  docs_migrations = [
 
71
  "Migration skipped (may already exist): %s.%s", table, column
72
  )
73
 
74
+ # Migrate api_keys
75
+ try:
76
+ existing_keys_columns = {c["name"] for c in inspector.get_columns("api_keys")}
77
+ except Exception:
78
+ existing_keys_columns = set()
79
+ keys_migrations = [
80
+ ("api_keys", "name", "ALTER TABLE api_keys ADD COLUMN name VARCHAR(100) DEFAULT 'default'"),
81
+ ("api_keys", "is_active", "ALTER TABLE api_keys ADD COLUMN is_active BOOLEAN DEFAULT 1 NOT NULL"),
82
+ ("api_keys", "last_used_at", "ALTER TABLE api_keys ADD COLUMN last_used_at TIMESTAMP"),
83
+ ]
84
+ for table, column, ddl in keys_migrations:
85
+ if column not in existing_keys_columns:
86
+ try:
87
+ with engine.begin() as conn:
88
+ conn.execute(text(ddl))
89
+ logger.info("Migration: added column %s.%s", table, column)
90
+ except Exception:
91
+ logger.warning(
92
+ "Migration skipped (may already exist): %s.%s", table, column
93
+ )
94
+
95
  # Migrate documents
96
  existing_docs_columns = {c["name"] for c in inspector.get_columns("documents")}
97
  docs_migrations = [
backend/app/models.py CHANGED
@@ -134,10 +134,12 @@ class ApiKey(Base):
134
 
135
  id = Column(GUID, primary_key=True, default=uuid.uuid4)
136
  user_id = Column(GUID, ForeignKey("users.id"), nullable=False, index=True)
137
- key_prefix = Column(String(10), nullable=False)
 
138
  hashed_key = Column(String(255), nullable=False, unique=True, index=True)
 
139
  created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
140
- last_used = Column(DateTime, nullable=True)
141
 
142
  # Relationships
143
  user = relationship("User", back_populates="api_keys")
 
134
 
135
  id = Column(GUID, primary_key=True, default=uuid.uuid4)
136
  user_id = Column(GUID, ForeignKey("users.id"), nullable=False, index=True)
137
+ name = Column(String(100), nullable=False, default="default")
138
+ key_prefix = Column(String(20), nullable=False)
139
  hashed_key = Column(String(255), nullable=False, unique=True, index=True)
140
+ is_active = Column(Boolean, default=True, nullable=False)
141
  created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
142
+ last_used_at = Column(DateTime, nullable=True)
143
 
144
  # Relationships
145
  user = relationship("User", back_populates="api_keys")
backend/app/routes/auth.py CHANGED
@@ -4,7 +4,7 @@ Auth API routes — register, login, and user profile.
4
  import re
5
  import secrets
6
  from datetime import datetime, timezone
7
- from fastapi import APIRouter, Depends, HTTPException, status
8
  from langsmith import expect
9
  from sqlalchemy.exc import SQLAlchemyError
10
  from sqlalchemy.orm import Session
@@ -419,26 +419,48 @@ from typing import List
419
  import hashlib
420
 
421
  @router.post("/api-keys", response_model=ApiKeyCreateResponse, status_code=status.HTTP_201_CREATED)
422
- def create_api_key(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
 
 
 
 
423
  """Create a new API key for the authenticated user."""
424
- raw_key = "rag_" + secrets.token_urlsafe(32)
 
425
  hashed_key = hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
426
-
427
  api_key = ApiKey(
428
  user_id=user.id,
429
- key_prefix=raw_key[:10],
 
430
  hashed_key=hashed_key,
 
431
  )
432
  db.add(api_key)
433
  db.commit()
434
  db.refresh(api_key)
435
-
436
- return {"key": raw_key, "api_key": api_key}
 
 
 
 
 
 
437
 
438
  @router.get("/api-keys", response_model=List[ApiKeyResponse])
439
  def list_api_keys(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
440
  """List all API keys for the authenticated user."""
441
- return db.query(ApiKey).filter(ApiKey.user_id == user.id).all()
 
 
 
 
 
 
 
 
 
442
 
443
  @router.delete("/api-keys/{key_id}", status_code=status.HTTP_204_NO_CONTENT)
444
  def delete_api_key(key_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
@@ -446,7 +468,7 @@ def delete_api_key(key_id: str, user: User = Depends(get_current_user), db: Sess
446
  api_key = db.query(ApiKey).filter(ApiKey.id == key_id, ApiKey.user_id == user.id).first()
447
  if not api_key:
448
  raise HTTPException(status_code=404, detail="API key not found")
449
-
450
  db.delete(api_key)
451
  db.commit()
452
  return None
 
4
  import re
5
  import secrets
6
  from datetime import datetime, timezone
7
+ from fastapi import APIRouter, Body, Depends, HTTPException, status
8
  from langsmith import expect
9
  from sqlalchemy.exc import SQLAlchemyError
10
  from sqlalchemy.orm import Session
 
419
  import hashlib
420
 
421
  @router.post("/api-keys", response_model=ApiKeyCreateResponse, status_code=status.HTTP_201_CREATED)
422
+ def create_api_key(
423
+ user: User = Depends(get_current_user),
424
+ db: Session = Depends(get_db),
425
+ body: dict = Body(None),
426
+ ):
427
  """Create a new API key for the authenticated user."""
428
+ name = (body or {}).get("name", "default")
429
+ raw_key = "pdf_rag_" + secrets.token_hex(24)
430
  hashed_key = hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
431
+
432
  api_key = ApiKey(
433
  user_id=user.id,
434
+ name=name,
435
+ key_prefix=raw_key[:15],
436
  hashed_key=hashed_key,
437
+ is_active=True,
438
  )
439
  db.add(api_key)
440
  db.commit()
441
  db.refresh(api_key)
442
+
443
+ return ApiKeyCreateResponse(
444
+ id=str(api_key.id),
445
+ name=api_key.name,
446
+ key_preview=api_key.key_prefix,
447
+ created_at=api_key.created_at,
448
+ raw_key=raw_key,
449
+ )
450
 
451
  @router.get("/api-keys", response_model=List[ApiKeyResponse])
452
  def list_api_keys(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
453
  """List all API keys for the authenticated user."""
454
+ keys = db.query(ApiKey).filter(ApiKey.user_id == user.id, ApiKey.is_active == True).all()
455
+ return [
456
+ ApiKeyResponse(
457
+ id=str(k.id),
458
+ name=k.name,
459
+ key_preview=k.key_prefix,
460
+ created_at=k.created_at,
461
+ )
462
+ for k in keys
463
+ ]
464
 
465
  @router.delete("/api-keys/{key_id}", status_code=status.HTTP_204_NO_CONTENT)
466
  def delete_api_key(key_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
 
468
  api_key = db.query(ApiKey).filter(ApiKey.id == key_id, ApiKey.user_id == user.id).first()
469
  if not api_key:
470
  raise HTTPException(status_code=404, detail="API key not found")
471
+
472
  db.delete(api_key)
473
  db.commit()
474
  return None
backend/app/schemas.py CHANGED
@@ -61,6 +61,7 @@ class HFTokenUpdate(BaseModel):
61
 
62
  class ApiKeyResponse(BaseModel):
63
  id: str
 
64
  key_preview: str
65
  created_at: datetime
66
 
@@ -68,9 +69,16 @@ class ApiKeyResponse(BaseModel):
68
  from_attributes = True
69
 
70
 
71
- class ApiKeyCreateResponse(ApiKeyResponse):
 
 
 
 
72
  raw_key: str
73
 
 
 
 
74
 
75
  class UserResponse(BaseModel):
76
  id: str
 
61
 
62
  class ApiKeyResponse(BaseModel):
63
  id: str
64
+ name: str
65
  key_preview: str
66
  created_at: datetime
67
 
 
69
  from_attributes = True
70
 
71
 
72
+ class ApiKeyCreateResponse(BaseModel):
73
+ id: str
74
+ name: str
75
+ key_preview: str
76
+ created_at: datetime
77
  raw_key: str
78
 
79
+ class Config:
80
+ from_attributes = True
81
+
82
 
83
  class UserResponse(BaseModel):
84
  id: str