Skip to content

Commit 8eb8b87

Browse files
committed
modified api key to function as per project
1 parent bc9a6b7 commit 8eb8b87

File tree

12 files changed

+201
-138
lines changed

12 files changed

+201
-138
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Add project_id in api_key table
2+
3+
Revision ID: 60b6c511a485
4+
Revises: 904ed70e7dab
5+
Create Date: 2025-06-10 13:22:59.947971
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlmodel import text
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "60b6c511a485"
15+
down_revision = "904ed70e7dab"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.add_column("apikey", sa.Column("project_id", sa.Integer(), nullable=True))
23+
op.create_foreign_key(
24+
None, "apikey", "project", ["project_id"], ["id"], ondelete="CASCADE"
25+
)
26+
27+
# 2. Bind engine to run raw SQL
28+
conn = op.get_bind()
29+
30+
# 3. Populate project_id based on default logic (e.g., min project id per org)
31+
conn.execute(
32+
text(
33+
"""
34+
UPDATE apikey
35+
SET project_id = (
36+
SELECT id FROM project
37+
WHERE project.organization_id = apikey.organization_id
38+
ORDER BY id ASC
39+
LIMIT 1
40+
)
41+
"""
42+
)
43+
)
44+
45+
# 4. Alter column to be NOT NULL now that it’s populated
46+
op.alter_column("apikey", "project_id", nullable=False)
47+
# ### end Alembic commands ###
48+
49+
50+
def downgrade():
51+
# ### commands auto generated by Alembic - please adjust! ###
52+
op.drop_constraint(None, "apikey", type_="foreignkey")
53+
op.drop_column("apikey", "project_id")
54+
# ### end Alembic commands ###

backend/app/api/routes/api_keys.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
from app.crud.api_key import (
66
create_api_key,
77
get_api_key,
8-
get_api_keys_by_organization,
98
delete_api_key,
10-
get_api_key_by_user_org,
9+
get_api_key_by_project,
1110
)
12-
from app.crud.organization import validate_organization
11+
from app.crud.project import validate_project
1312
from app.models import APIKeyPublic, User
1413
from app.utils import APIResponse
1514

@@ -19,7 +18,7 @@
1918
# Create API Key
2019
@router.post("/", response_model=APIResponse[APIKeyPublic])
2120
def create_key(
22-
organization_id: int,
21+
project_id: int,
2322
user_id: uuid.UUID,
2423
session: Session = Depends(get_db),
2524
current_user: User = Depends(get_current_active_superuser),
@@ -29,18 +28,21 @@ def create_key(
2928
"""
3029
try:
3130
# Validate organization
32-
validate_organization(session, organization_id)
31+
project = validate_project(session, project_id)
3332

34-
existing_api_key = get_api_key_by_user_org(session, organization_id, user_id)
33+
existing_api_key = get_api_key_by_project(session, project_id)
3534
if existing_api_key:
3635
raise HTTPException(
3736
status_code=400,
38-
detail="API Key already exists for this user and organization",
37+
detail="API Key already exists for this user and project.",
3938
)
4039

4140
# Create and return API key
4241
api_key = create_api_key(
43-
session, organization_id=organization_id, user_id=user_id
42+
session,
43+
organization_id=project.organization_id,
44+
user_id=user_id,
45+
project_id=project_id,
4446
)
4547
return APIResponse.success_response(api_key)
4648

@@ -49,22 +51,25 @@ def create_key(
4951

5052

5153
# List API Keys
52-
@router.get("/", response_model=APIResponse[list[APIKeyPublic]])
54+
@router.get("/", response_model=APIResponse[APIKeyPublic])
5355
def list_keys(
54-
organization_id: int,
56+
project_id: int,
5557
session: Session = Depends(get_db),
5658
current_user: User = Depends(get_current_active_superuser),
5759
):
5860
"""
59-
Retrieve all API keys for the user's organization.
61+
Retrieve all API keys for the given project.
6062
"""
6163
try:
62-
# Validate organization
63-
validate_organization(session, organization_id)
64+
# Validate project
65+
project = validate_project(session=session, project_id=project_id)
66+
67+
# Retrieve the API key for this project (regardless of user)
68+
api_key = get_api_key_by_project(session=session, project_id=project_id)
69+
if not api_key:
70+
raise HTTPException(status_code=404, detail="API key not found for project")
6471

65-
# Retrieve API keys
66-
api_keys = get_api_keys_by_organization(session, organization_id)
67-
return APIResponse.success_response(api_keys)
72+
return APIResponse.success_response(api_key)
6873

6974
except ValueError as e:
7075
raise HTTPException(status_code=400, detail=str(e))

backend/app/api/routes/onboarding.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,17 @@
1010
create_project,
1111
create_user,
1212
create_api_key,
13-
get_api_key_by_user_org,
13+
get_api_key_by_project,
1414
)
1515
from app.models import (
1616
OrganizationCreate,
1717
ProjectCreate,
1818
UserCreate,
19-
APIKeyPublic,
20-
Organization,
2119
Project,
2220
User,
2321
APIKey,
2422
)
25-
from app.core.security import get_password_hash
2623
from app.api.deps import (
27-
CurrentUser,
2824
SessionDep,
2925
get_current_active_superuser,
3026
)
@@ -90,18 +86,19 @@ def onboard_user(request: OnboardingRequest, session: SessionDep):
9086
)
9187
user = create_user(session=session, user_create=user_create)
9288

93-
existing_key = get_api_key_by_user_org(
94-
db=session, organization_id=organization.id, user_id=user.id
95-
)
89+
existing_key = get_api_key_by_project(session=session, project_id=project.id)
9690

9791
if existing_key:
9892
raise HTTPException(
9993
status_code=400,
100-
detail="API key already exists for this user and organization",
94+
detail="API key already exists for this user and project.",
10195
)
10296

10397
api_key_public = create_api_key(
104-
session=session, organization_id=organization.id, user_id=user.id
98+
session=session,
99+
organization_id=organization.id,
100+
user_id=user.id,
101+
project_id=project.id,
105102
)
106103

107104
user.is_superuser = False

backend/app/crud/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,8 @@
2424
from .api_key import (
2525
create_api_key,
2626
get_api_key,
27-
get_api_key_by_user_org,
2827
get_api_key_by_value,
29-
get_api_keys_by_organization,
28+
get_api_key_by_project,
3029
delete_api_key,
3130
)
3231

backend/app/crud/api_key.py

Lines changed: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
import uuid
22
import secrets
3-
from datetime import datetime, timezone
43
from sqlmodel import Session, select
54
from app.core.security import (
6-
verify_password,
75
get_password_hash,
86
encrypt_api_key,
97
decrypt_api_key,
108
)
11-
import base64
12-
from cryptography.hazmat.primitives import hashes
13-
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
149
from app.core import settings
1510
from app.core.util import now
16-
1711
from app.models.api_key import APIKey, APIKeyPublic
1812

1913

@@ -25,7 +19,7 @@ def generate_api_key() -> tuple[str, str]:
2519

2620

2721
def create_api_key(
28-
session: Session, organization_id: uuid.UUID, user_id: uuid.UUID
22+
session: Session, organization_id: int, user_id: uuid.UUID, project_id: int
2923
) -> APIKeyPublic:
3024
"""
3125
Generates a new API key for an organization and associates it with a user.
@@ -42,6 +36,7 @@ def create_api_key(
4236
key=encrypted_key, # Store the encrypted raw key
4337
organization_id=organization_id,
4438
user_id=user_id,
39+
project_id=project_id,
4540
)
4641

4742
session.add(api_key)
@@ -75,32 +70,6 @@ def get_api_key(session: Session, api_key_id: int) -> APIKeyPublic | None:
7570
return None
7671

7772

78-
def get_api_keys_by_organization(
79-
session: Session, organization_id: uuid.UUID
80-
) -> list[APIKeyPublic]:
81-
"""
82-
Retrieves all active API keys associated with an organization.
83-
Returns the API keys in their original format.
84-
"""
85-
api_keys = session.exec(
86-
select(APIKey).where(
87-
APIKey.organization_id == organization_id, APIKey.is_deleted == False
88-
)
89-
).all()
90-
91-
raw_keys = []
92-
for api_key in api_keys:
93-
api_key_dict = api_key.model_dump()
94-
95-
decrypted_key = decrypt_api_key(api_key.key)
96-
97-
api_key_dict["key"] = decrypted_key
98-
99-
raw_keys.append(APIKeyPublic.model_validate(api_key_dict))
100-
101-
return raw_keys
102-
103-
10473
def delete_api_key(session: Session, api_key_id: int) -> None:
10574
"""
10675
Soft deletes (revokes) an API key by marking it as deleted.
@@ -137,23 +106,18 @@ def get_api_key_by_value(session: Session, api_key_value: str) -> APIKeyPublic |
137106
return None
138107

139108

140-
def get_api_key_by_user_org(
141-
db: Session, organization_id: int, user_id: int
142-
) -> APIKey | None:
143-
"""Get an API key by user and organization ID."""
109+
def get_api_key_by_project(session: Session, project_id: int) -> APIKeyPublic | None:
110+
"""
111+
Retrieves the single API key associated with a project.
112+
"""
144113
statement = select(APIKey).where(
145-
APIKey.organization_id == organization_id,
146-
APIKey.user_id == user_id,
147-
APIKey.is_deleted == False,
114+
APIKey.project_id == project_id, APIKey.is_deleted == False
148115
)
149-
api_key = db.exec(statement).first()
116+
api_key = session.exec(statement).first()
150117

151118
if api_key:
152119
api_key_dict = api_key.model_dump()
120+
api_key_dict["key"] = decrypt_api_key(api_key.key)
121+
return APIKeyPublic.model_validate(api_key_dict)
153122

154-
decrypted_key = decrypt_api_key(api_key.key)
155-
156-
api_key_dict["key"] = decrypted_key
157-
158-
return APIKey.model_validate(api_key_dict)
159123
return None

backend/app/crud/project.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from datetime import datetime, timezone
33
from sqlmodel import Session, select
44

5-
from app.models import Project, ProjectCreate
5+
from app.models import Project, ProjectCreate, Organization
66
from app.core.util import now
77

88

@@ -24,3 +24,17 @@ def get_project_by_id(*, session: Session, project_id: int) -> Optional[Project]
2424
def get_projects_by_organization(*, session: Session, org_id: int) -> List[Project]:
2525
statement = select(Project).where(Project.organization_id == org_id)
2626
return session.exec(statement).all()
27+
28+
29+
def validate_project(session: Session, project_id: int) -> Project:
30+
"""
31+
Ensures that an project exists and is active.
32+
"""
33+
project = get_project_by_id(session=session, project_id=project_id)
34+
if not project:
35+
raise ValueError("Organization not found")
36+
37+
if not project.is_active:
38+
raise ValueError("Organization is not active")
39+
40+
return project

backend/app/models/api_key.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ class APIKeyBase(SQLModel):
1111
organization_id: int = Field(
1212
foreign_key="organization.id", nullable=False, ondelete="CASCADE"
1313
)
14+
project_id: int = Field(
15+
foreign_key="project.id", nullable=False, ondelete="CASCADE"
16+
)
1417
user_id: uuid.UUID = Field(
1518
foreign_key="user.id", nullable=False, ondelete="CASCADE"
1619
)
@@ -33,4 +36,5 @@ class APIKey(APIKeyBase, table=True):
3336

3437
# Relationships
3538
organization: "Organization" = Relationship(back_populates="api_keys")
39+
project: "Project" = Relationship(back_populates="api_keys")
3640
user: "User" = Relationship(back_populates="api_keys")

backend/app/models/project.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class Project(ProjectBase, table=True):
3737
creds: list["Credential"] = Relationship(
3838
back_populates="project", sa_relationship_kwargs={"cascade": "all, delete"}
3939
)
40+
api_keys: list["APIKey"] = Relationship(
41+
back_populates="project", sa_relationship_kwargs={"cascade": "all, delete"}
42+
)
4043
organization: Optional["Organization"] = Relationship(back_populates="project")
4144

4245

0 commit comments

Comments
 (0)