GitHub Actions commited on
Commit
86d79a3
·
1 Parent(s): 76ca861

🚀 Auto-deploy from GitHub

Browse files
app/api/v1/endpoints/auth.py CHANGED
@@ -2,14 +2,27 @@
2
  Authentication endpoints for token management
3
  """
4
  from fastapi import APIRouter, HTTPException, Depends
 
5
  from datetime import datetime, timedelta
6
  from typing import Dict, Any
7
  import jwt
8
  import os
9
- from ..schemas.auth_schemas import TokenRequest, TokenResponse
 
10
  from ....core.config import settings
 
 
 
 
 
 
 
 
 
 
11
 
12
  router = APIRouter()
 
13
 
14
  @router.post("/auth/token", response_model=TokenResponse)
15
  async def get_access_token(credentials: TokenRequest):
@@ -17,16 +30,27 @@ async def get_access_token(credentials: TokenRequest):
17
  Exchange user credentials for a temporary access token
18
  This endpoint allows users to authenticate and receive a JWT token
19
  """
20
- # Example: validate user credentials against your user database
21
- # For now, this is just a placeholder implementation
22
-
23
- if not validate_user_credentials(credentials.dict()):
24
  raise HTTPException(status_code=401, detail="Invalid credentials")
25
 
 
 
 
 
 
 
 
 
 
 
26
  # Generate a temporary JWT token
27
  payload = {
28
  "sub": credentials.username,
29
- "exp": datetime.utcnow() + timedelta(hours=24),
 
 
30
  "iat": datetime.utcnow(),
31
  "type": "access_token"
32
  }
@@ -71,28 +95,68 @@ async def refresh_access_token(refresh_token: str):
71
  except jwt.InvalidTokenError:
72
  raise HTTPException(status_code=401, detail="Invalid refresh token")
73
 
74
- def validate_user_credentials(credentials: Dict[str, Any]) -> bool:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  """
76
- Validate user credentials against your user database
 
77
 
78
- TODO: Implement your actual user validation logic here
79
- This could check against:
80
- - Database users table
81
- - LDAP/Active Directory
82
- - OAuth provider
83
- - etc.
 
 
 
 
 
 
 
 
 
 
 
 
84
  """
85
- # Placeholder implementation - replace with your actual auth logic
86
  username = credentials.get("username")
87
  password = credentials.get("password")
88
 
89
- # Example: simple hardcoded validation (NOT for production!)
90
- if username == "demo" and password == "demo123":
91
- return True
 
 
 
 
92
 
93
- # In a real implementation, you would:
94
- # 1. Hash the password and compare with stored hash
95
- # 2. Check against your user database
96
- # 3. Validate any additional requirements
97
 
98
- return False
 
2
  Authentication endpoints for token management
3
  """
4
  from fastapi import APIRouter, HTTPException, Depends
5
+ from fastapi.security import HTTPBearer
6
  from datetime import datetime, timedelta
7
  from typing import Dict, Any
8
  import jwt
9
  import os
10
+ import uuid
11
+ from ..schemas.auth_schemas import TokenRequest, TokenResponse, UserRegistrationRequest, UserResponse
12
  from ....core.config import settings
13
+ from ....services.database import (
14
+ get_user_by_username,
15
+ get_user_by_email,
16
+ create_user,
17
+ verify_password,
18
+ update_last_login,
19
+ create_user_session,
20
+ get_user_session,
21
+ revoke_user_session
22
+ )
23
 
24
  router = APIRouter()
25
+ security = HTTPBearer(auto_error=False)
26
 
27
  @router.post("/auth/token", response_model=TokenResponse)
28
  async def get_access_token(credentials: TokenRequest):
 
30
  Exchange user credentials for a temporary access token
31
  This endpoint allows users to authenticate and receive a JWT token
32
  """
33
+ # Validate user credentials against your Supabase database
34
+ user = await validate_user_credentials(credentials.dict())
35
+ if not user:
 
36
  raise HTTPException(status_code=401, detail="Invalid credentials")
37
 
38
+ # Update last login timestamp
39
+ await update_last_login(user["id"])
40
+
41
+ # Generate unique JTI (JWT ID) for session tracking
42
+ jti = str(uuid.uuid4())
43
+ expires_at = datetime.utcnow() + timedelta(hours=24)
44
+
45
+ # Create session in database
46
+ await create_user_session(user["id"], jti, expires_at)
47
+
48
  # Generate a temporary JWT token
49
  payload = {
50
  "sub": credentials.username,
51
+ "user_id": user["id"],
52
+ "jti": jti,
53
+ "exp": expires_at,
54
  "iat": datetime.utcnow(),
55
  "type": "access_token"
56
  }
 
95
  except jwt.InvalidTokenError:
96
  raise HTTPException(status_code=401, detail="Invalid refresh token")
97
 
98
+ @router.post("/auth/register", response_model=UserResponse)
99
+ async def register_user(user_data: UserRegistrationRequest):
100
+ """
101
+ Register a new user account
102
+ """
103
+ # Check if username already exists
104
+ existing_user = await get_user_by_username(user_data.username)
105
+ if existing_user:
106
+ raise HTTPException(status_code=400, detail="Username already exists")
107
+
108
+ # Check if email already exists
109
+ existing_email = await get_user_by_email(user_data.email)
110
+ if existing_email:
111
+ raise HTTPException(status_code=400, detail="Email already registered")
112
+
113
+ # Create new user
114
+ new_user = await create_user(user_data.username, user_data.email, user_data.password)
115
+ if not new_user:
116
+ raise HTTPException(status_code=500, detail="Failed to create user")
117
+
118
+ return UserResponse(**new_user)
119
+
120
+ @router.post("/auth/logout")
121
+ async def logout_user(token: str = Depends(security)):
122
+ """
123
+ Logout user by revoking their session
124
  """
125
+ if not token:
126
+ raise HTTPException(status_code=401, detail="No token provided")
127
 
128
+ try:
129
+ secret_key = os.getenv("SECRET_KEY", "your-secret-key-change-this")
130
+ payload = jwt.decode(token.credentials, secret_key, algorithms=["HS256"])
131
+ jti = payload.get("jti")
132
+
133
+ if jti:
134
+ await revoke_user_session(jti)
135
+ return {"message": "Successfully logged out"}
136
+ else:
137
+ raise HTTPException(status_code=400, detail="Invalid token format")
138
+
139
+ except jwt.InvalidTokenError:
140
+ raise HTTPException(status_code=401, detail="Invalid token")
141
+
142
+ async def validate_user_credentials(credentials: Dict[str, Any]) -> Dict[str, Any] | None:
143
+ """
144
+ Validate user credentials against your Supabase database
145
+ Returns user data if valid, None if invalid
146
  """
 
147
  username = credentials.get("username")
148
  password = credentials.get("password")
149
 
150
+ if not username or not password:
151
+ return None
152
+
153
+ # Get user from database
154
+ user = await get_user_by_username(username)
155
+ if not user:
156
+ return None
157
 
158
+ # Verify password
159
+ if not await verify_password(password, user["password_hash"]):
160
+ return None
 
161
 
162
+ return user
app/api/v1/schemas/auth_schemas.py CHANGED
@@ -1,8 +1,9 @@
1
  """
2
  Authentication schemas for request/response models
3
  """
4
- from pydantic import BaseModel
5
  from typing import Optional
 
6
 
7
  class TokenRequest(BaseModel):
8
  """Request model for token authentication"""
@@ -18,3 +19,17 @@ class TokenResponse(BaseModel):
18
  class RefreshTokenRequest(BaseModel):
19
  """Request model for token refresh"""
20
  refresh_token: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  Authentication schemas for request/response models
3
  """
4
+ from pydantic import BaseModel, EmailStr
5
  from typing import Optional
6
+ from datetime import datetime
7
 
8
  class TokenRequest(BaseModel):
9
  """Request model for token authentication"""
 
19
  class RefreshTokenRequest(BaseModel):
20
  """Request model for token refresh"""
21
  refresh_token: str
22
+
23
+ class UserRegistrationRequest(BaseModel):
24
+ """Request model for user registration"""
25
+ username: str
26
+ email: EmailStr
27
+ password: str
28
+
29
+ class UserResponse(BaseModel):
30
+ """Response model for user data (without sensitive info)"""
31
+ id: str
32
+ username: str
33
+ email: str
34
+ created_at: datetime
35
+ last_login: Optional[datetime] = None
app/core/auth.py CHANGED
@@ -8,6 +8,7 @@ import os
8
  import jwt
9
  from datetime import datetime
10
  from dotenv import load_dotenv
 
11
 
12
  load_dotenv()
13
 
@@ -17,7 +18,7 @@ def get_hf_api_key() -> str:
17
  """Get HuggingFace API key from environment"""
18
  return os.getenv("HF_API_KEY", "")
19
 
20
- def verify_hf_token(credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)) -> bool:
21
  """
22
  Verify HuggingFace API token or JWT token
23
  Returns True if no authentication is required (public space) or if token is valid
@@ -46,9 +47,22 @@ def verify_hf_token(credentials: Optional[HTTPAuthorizationCredentials] = Depend
46
  try:
47
  secret_key = os.getenv("SECRET_KEY", "your-secret-key")
48
  payload = jwt.decode(token, secret_key, algorithms=["HS256"])
 
49
  # Verify token hasn't expired
50
  if payload.get("exp") and datetime.fromtimestamp(payload["exp"]) < datetime.utcnow():
51
  raise jwt.ExpiredSignatureError("Token has expired")
 
 
 
 
 
 
 
 
 
 
 
 
52
  return True
53
  except jwt.InvalidTokenError:
54
  pass
@@ -60,7 +74,7 @@ def verify_hf_token(credentials: Optional[HTTPAuthorizationCredentials] = Depend
60
  headers={"WWW-Authenticate": "Bearer"},
61
  )
62
 
63
- def optional_auth(credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)) -> bool:
64
  """
65
  Optional authentication - doesn't raise errors if no token provided
66
  Used for endpoints that can work with or without authentication
@@ -76,4 +90,26 @@ def optional_auth(credentials: Optional[HTTPAuthorizationCredentials] = Depends(
76
  return False
77
 
78
  # If credentials provided, verify them
79
- return credentials.credentials == expected_token
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  import jwt
9
  from datetime import datetime
10
  from dotenv import load_dotenv
11
+ from ..services.database import get_user_session
12
 
13
  load_dotenv()
14
 
 
18
  """Get HuggingFace API key from environment"""
19
  return os.getenv("HF_API_KEY", "")
20
 
21
+ async def verify_hf_token(credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)) -> bool:
22
  """
23
  Verify HuggingFace API token or JWT token
24
  Returns True if no authentication is required (public space) or if token is valid
 
47
  try:
48
  secret_key = os.getenv("SECRET_KEY", "your-secret-key")
49
  payload = jwt.decode(token, secret_key, algorithms=["HS256"])
50
+
51
  # Verify token hasn't expired
52
  if payload.get("exp") and datetime.fromtimestamp(payload["exp"]) < datetime.utcnow():
53
  raise jwt.ExpiredSignatureError("Token has expired")
54
+
55
+ # Check if session is still valid (not revoked)
56
+ jti = payload.get("jti")
57
+ if jti:
58
+ session = await get_user_session(jti)
59
+ if not session:
60
+ raise HTTPException(
61
+ status_code=status.HTTP_401_UNAUTHORIZED,
62
+ detail="Session has been revoked",
63
+ headers={"WWW-Authenticate": "Bearer"},
64
+ )
65
+
66
  return True
67
  except jwt.InvalidTokenError:
68
  pass
 
74
  headers={"WWW-Authenticate": "Bearer"},
75
  )
76
 
77
+ async def optional_auth(credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)) -> bool:
78
  """
79
  Optional authentication - doesn't raise errors if no token provided
80
  Used for endpoints that can work with or without authentication
 
90
  return False
91
 
92
  # If credentials provided, verify them
93
+ token = credentials.credentials
94
+
95
+ # Check HF API key
96
+ if token == expected_token:
97
+ return True
98
+
99
+ # Check JWT token
100
+ try:
101
+ secret_key = os.getenv("SECRET_KEY", "your-secret-key")
102
+ payload = jwt.decode(token, secret_key, algorithms=["HS256"])
103
+
104
+ if payload.get("exp") and datetime.fromtimestamp(payload["exp"]) < datetime.utcnow():
105
+ return False
106
+
107
+ # Check session validity
108
+ jti = payload.get("jti")
109
+ if jti:
110
+ session = await get_user_session(jti)
111
+ return session is not None
112
+
113
+ return True
114
+ except jwt.InvalidTokenError:
115
+ return False
app/services/database.py CHANGED
@@ -1,6 +1,10 @@
1
  import os
2
  from supabase import create_client
3
  from dotenv import load_dotenv
 
 
 
 
4
 
5
  load_dotenv()
6
 
@@ -36,3 +40,108 @@ async def save_card(supabase_client, card_data: dict):
36
  # Hier wäre ein besseres Logging/Fehlerhandling gut
37
  print(f"Error saving to Supabase: {e}")
38
  raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  from supabase import create_client
3
  from dotenv import load_dotenv
4
+ import bcrypt
5
+ import uuid
6
+ from datetime import datetime, timedelta
7
+ from typing import Optional, Dict, Any
8
 
9
  load_dotenv()
10
 
 
40
  # Hier wäre ein besseres Logging/Fehlerhandling gut
41
  print(f"Error saving to Supabase: {e}")
42
  raise
43
+
44
+ # User management functions
45
+ async def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
46
+ """Get user by username from the database"""
47
+ try:
48
+ response = supabase.table("users").select("*").eq("username", username).execute()
49
+ if response.data:
50
+ return response.data[0]
51
+ return None
52
+ except Exception as e:
53
+ print(f"Error getting user by username: {e}")
54
+ return None
55
+
56
+ async def get_user_by_email(email: str) -> Optional[Dict[str, Any]]:
57
+ """Get user by email from the database"""
58
+ try:
59
+ response = supabase.table("users").select("*").eq("email", email).execute()
60
+ if response.data:
61
+ return response.data[0]
62
+ return None
63
+ except Exception as e:
64
+ print(f"Error getting user by email: {e}")
65
+ return None
66
+
67
+ async def create_user(username: str, email: str, password: str) -> Optional[Dict[str, Any]]:
68
+ """Create a new user with hashed password"""
69
+ try:
70
+ # Hash the password
71
+ password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
72
+
73
+ user_data = {
74
+ "username": username,
75
+ "email": email,
76
+ "password_hash": password_hash
77
+ }
78
+
79
+ response = supabase.table("users").insert(user_data).execute()
80
+ if response.data:
81
+ return response.data[0]
82
+ return None
83
+ except Exception as e:
84
+ print(f"Error creating user: {e}")
85
+ return None
86
+
87
+ async def verify_password(plain_password: str, hashed_password: str) -> bool:
88
+ """Verify a password against its hash"""
89
+ try:
90
+ return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
91
+ except Exception as e:
92
+ print(f"Error verifying password: {e}")
93
+ return False
94
+
95
+ async def update_last_login(user_id: str):
96
+ """Update the last login timestamp for a user"""
97
+ try:
98
+ from datetime import datetime
99
+ current_time = datetime.utcnow().isoformat()
100
+ supabase.table("users").update({"last_login": current_time}).eq("id", user_id).execute()
101
+ except Exception as e:
102
+ print(f"Error updating last login: {e}")
103
+
104
+ # Session management functions
105
+ async def create_user_session(user_id: str, token_jti: str, expires_at: datetime) -> Optional[Dict[str, Any]]:
106
+ """Create a new user session"""
107
+ try:
108
+ session_data = {
109
+ "user_id": user_id,
110
+ "token_jti": token_jti,
111
+ "expires_at": expires_at.isoformat(),
112
+ "is_revoked": False
113
+ }
114
+
115
+ response = supabase.table("user_sessions").insert(session_data).execute()
116
+ if response.data:
117
+ return response.data[0]
118
+ return None
119
+ except Exception as e:
120
+ print(f"Error creating user session: {e}")
121
+ return None
122
+
123
+ async def get_user_session(token_jti: str) -> Optional[Dict[str, Any]]:
124
+ """Get user session by token JTI"""
125
+ try:
126
+ response = supabase.table("user_sessions").select("*").eq("token_jti", token_jti).eq("is_revoked", False).execute()
127
+ if response.data:
128
+ return response.data[0]
129
+ return None
130
+ except Exception as e:
131
+ print(f"Error getting user session: {e}")
132
+ return None
133
+
134
+ async def revoke_user_session(token_jti: str):
135
+ """Revoke a user session"""
136
+ try:
137
+ supabase.table("user_sessions").update({"is_revoked": True}).eq("token_jti", token_jti).execute()
138
+ except Exception as e:
139
+ print(f"Error revoking user session: {e}")
140
+
141
+ async def cleanup_expired_sessions():
142
+ """Remove expired sessions from the database"""
143
+ try:
144
+ current_time = datetime.utcnow().isoformat()
145
+ supabase.table("user_sessions").delete().lt("expires_at", current_time).execute()
146
+ except Exception as e:
147
+ print(f"Error cleaning up expired sessions: {e}")