1010from airweave .api .context import ApiContext
1111from airweave .api .router import TrailingSlashRouter
1212from airweave .core import credentials
13+ from airweave .core .datetime_utils import utc_now_naive
1314
1415router = TrailingSlashRouter ()
1516
1819async def create_api_key (
1920 * ,
2021 db : AsyncSession = Depends (deps .get_db ),
21- api_key_in : schemas .APIKeyCreate = Body ({}), # Default to empty dict if not provided
22+ api_key_in : schemas .APIKeyCreate = Body (default_factory = lambda : schemas . APIKeyCreate ()),
2223 ctx : ApiContext = Depends (deps .get_context ),
2324) -> schemas .APIKey :
2425 """Create a new API key for the current user.
@@ -39,13 +40,22 @@ async def create_api_key(
3940 """
4041 api_key_obj = await crud .api_key .create (db = db , obj_in = api_key_in , ctx = ctx )
4142
43+ # Audit log: API key creation (flows to Azure LAW)
44+ expiration_days = (api_key_obj .expiration_date - api_key_obj .created_at ).days
45+ audit_logger = ctx .logger .with_context (event_type = "api_key_created" )
46+ audit_logger .info (
47+ f"API key created: { api_key_obj .id } by { api_key_obj .created_by_email } "
48+ f"for org { ctx .organization .id } , expires in { expiration_days } days "
49+ f"({ api_key_obj .expiration_date .isoformat ()} )"
50+ )
51+
4252 # Decrypt the key for the response
4353 decrypted_data = credentials .decrypt (api_key_obj .encrypted_key )
4454 decrypted_key = decrypted_data ["key" ]
4555
4656 api_key_data = {
4757 "id" : api_key_obj .id ,
48- "organization " : ctx .organization .id , # Use the user's organization_id
58+ "organization_id " : ctx .organization .id ,
4959 "created_at" : api_key_obj .created_at ,
5060 "modified_at" : api_key_obj .modified_at ,
5161 "last_used_date" : None , # New key has no last used date
@@ -88,7 +98,7 @@ async def read_api_key(
8898
8999 api_key_data = {
90100 "id" : api_key .id ,
91- "organization " : ctx .organization .id ,
101+ "organization_id " : ctx .organization .id ,
92102 "created_at" : api_key .created_at ,
93103 "modified_at" : api_key .modified_at ,
94104 "last_used_date" : api_key .last_used_date if hasattr (api_key , "last_used_date" ) else None ,
@@ -132,7 +142,7 @@ async def read_api_keys(
132142
133143 api_key_data = {
134144 "id" : api_key .id ,
135- "organization " : ctx .organization .id ,
145+ "organization_id " : ctx .organization .id ,
136146 "created_at" : api_key .created_at ,
137147 "modified_at" : api_key .modified_at ,
138148 "last_used_date" : (
@@ -148,6 +158,63 @@ async def read_api_keys(
148158 return result
149159
150160
161+ @router .post ("/{id}/rotate" , response_model = schemas .APIKey )
162+ async def rotate_api_key (
163+ * ,
164+ db : AsyncSession = Depends (deps .get_db ),
165+ id : UUID ,
166+ ctx : ApiContext = Depends (deps .get_context ),
167+ ) -> schemas .APIKey :
168+ """Rotate an API key by creating a new one.
169+
170+ This endpoint creates a new API key with a fresh 90-day expiration.
171+ The old key remains active until its original expiration date.
172+ Users can manage multiple keys or delete the old one manually if desired.
173+
174+ Args:
175+ ----
176+ db (AsyncSession): The database session.
177+ id (UUID): The ID of the API key to rotate.
178+ ctx (ApiContext): The current authentication context.
179+
180+ Returns:
181+ -------
182+ schemas.APIKey: The newly created API key with decrypted key value.
183+
184+ Raises:
185+ ------
186+ HTTPException: If the API key is not found or user doesn't have access.
187+
188+ """
189+ # Verify old key exists and user has access
190+ old_key = await crud .api_key .get (db = db , id = id , ctx = ctx )
191+ old_key_schema = schemas .APIKey .model_validate (old_key , from_attributes = True )
192+
193+ # Create new key with default 90-day expiration
194+ new_key_obj = await crud .api_key .create (
195+ db = db ,
196+ obj_in = schemas .APIKeyCreate (), # Uses default 90 days
197+ ctx = ctx ,
198+ )
199+
200+ # Decrypt the new key for the response
201+ decrypted_data = credentials .decrypt (new_key_obj .encrypted_key )
202+ decrypted_key = decrypted_data ["key" ]
203+
204+ new_key_schema = schemas .APIKey .model_validate (new_key_obj , from_attributes = True )
205+ new_key_schema .decrypted_key = decrypted_key
206+
207+ # Audit log: API key rotation (flows to Azure LAW)
208+ audit_logger = ctx .logger .with_context (event_type = "api_key_rotated" )
209+ audit_logger .info (
210+ f"API key rotated: old={ old_key_schema .id } , new={ new_key_schema .id } "
211+ f"by { new_key_schema .created_by_email } for org { ctx .organization .id } , "
212+ f"new key expires { new_key_schema .expiration_date .isoformat ()} "
213+ )
214+
215+ return new_key_schema
216+
217+
151218@router .delete ("/" , response_model = schemas .APIKey )
152219async def delete_api_key (
153220 * ,
@@ -181,7 +248,7 @@ async def delete_api_key(
181248 # Create a copy of the data before deletion
182249 api_key_data = {
183250 "id" : api_key .id ,
184- "organization " : ctx .organization .id ,
251+ "organization_id " : ctx .organization .id ,
185252 "created_at" : api_key .created_at ,
186253 "modified_at" : api_key .modified_at ,
187254 "last_used_date" : api_key .last_used_date if hasattr (api_key , "last_used_date" ) else None ,
@@ -191,6 +258,14 @@ async def delete_api_key(
191258 "decrypted_key" : decrypted_key ,
192259 }
193260
261+ # Audit log: API key deletion (flows to Azure LAW)
262+ was_expired = api_key .expiration_date < utc_now_naive ()
263+ audit_logger = ctx .logger .with_context (event_type = "api_key_deleted" )
264+ audit_logger .info (
265+ f"API key deleted: { api_key .id } by { ctx .tracking_email } for org { ctx .organization .id } "
266+ f"(was_expired={ was_expired } )"
267+ )
268+
194269 # Now delete the API key
195270 await crud .api_key .remove (db = db , id = id , ctx = ctx )
196271
0 commit comments