@@ -44,6 +44,8 @@ def ensure_column(col, col_def):
4444 ensure_column ('notify_email' , 'notify_email INTEGER DEFAULT 1' )
4545 ensure_column ('notify_digest' , "notify_digest TEXT DEFAULT 'weekly'" )
4646 ensure_column ('notify_webhook' , 'notify_webhook TEXT' )
47+ # admin flag
48+ ensure_column ('is_admin' , 'is_admin INTEGER DEFAULT 0' )
4749 conn .commit ()
4850 conn .close ()
4951 # ensure API tokens table exists
@@ -52,6 +54,7 @@ def ensure_column(col, col_def):
5254 ensure_groups_tables ()
5355 ensure_projects_tables ()
5456 ensure_environments_table ()
57+ ensure_audit_table ()
5558
5659
5760def ensure_api_tokens_table ():
@@ -155,6 +158,20 @@ def ensure_projects_tables():
155158 ''' )
156159 conn .commit ()
157160 conn .close ()
161+ # project members table
162+ conn = get_connection ()
163+ cur = conn .cursor ()
164+ cur .execute ('''
165+ CREATE TABLE IF NOT EXISTS project_members (
166+ id INTEGER PRIMARY KEY AUTOINCREMENT,
167+ project_id INTEGER NOT NULL,
168+ user_id INTEGER NOT NULL,
169+ role TEXT NOT NULL,
170+ added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
171+ )
172+ ''' )
173+ conn .commit ()
174+ conn .close ()
158175
159176
160177def ensure_environments_table ():
@@ -182,13 +199,34 @@ def ensure_environments_table():
182199 conn .close ()
183200
184201
202+ def ensure_audit_table ():
203+ conn = get_connection ()
204+ cur = conn .cursor ()
205+ cur .execute ('''
206+ CREATE TABLE IF NOT EXISTS audit_logs (
207+ id INTEGER PRIMARY KEY AUTOINCREMENT,
208+ event_type TEXT NOT NULL,
209+ actor_user_id INTEGER,
210+ details TEXT,
211+ ip TEXT,
212+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
213+ )
214+ ''' )
215+ conn .commit ()
216+ conn .close ()
217+
218+
185219def create_environment (project_id : int , name : str , target : str = None ):
186220 conn = get_connection ()
187221 cur = conn .cursor ()
188222 cur .execute ('INSERT INTO environments (project_id, name, target) VALUES (?, ?, ?)' , (project_id , name , target ))
189223 conn .commit ()
190224 eid = cur .lastrowid
191225 conn .close ()
226+ try :
227+ record_audit ('environment.create' , actor_user_id = None , details = f'project_id={ project_id } , env={ name } ' )
228+ except Exception :
229+ pass
192230 return eid
193231
194232
@@ -221,13 +259,35 @@ def list_env_vars(environment_id: int):
221259 conn .close ()
222260 return [dict (r ) for r in rows ]
223261
262+
263+ def record_audit (event_type : str , actor_user_id : int = None , details : str = None , ip : str = None ):
264+ """Record an audit event."""
265+ conn = get_connection ()
266+ cur = conn .cursor ()
267+ cur .execute ('INSERT INTO audit_logs (event_type, actor_user_id, details, ip) VALUES (?, ?, ?, ?)' , (event_type , actor_user_id , details , ip ))
268+ conn .commit ()
269+ conn .close ()
270+
271+
272+ def list_audit_logs (limit : int = 200 ):
273+ conn = get_connection ()
274+ cur = conn .cursor ()
275+ cur .execute ('SELECT id, event_type, actor_user_id, details, ip, created_at FROM audit_logs ORDER BY created_at DESC LIMIT ?' , (limit ,))
276+ rows = cur .fetchall ()
277+ conn .close ()
278+ return [dict (r ) for r in rows ]
279+
224280def create_project (name : str , repo_url : str = None , orchestration : str = None , config : str = None ):
225281 conn = get_connection ()
226282 cur = conn .cursor ()
227283 cur .execute ('INSERT INTO projects (name, repo_url, orchestration, config) VALUES (?, ?, ?, ?)' , (name , repo_url , orchestration , config ))
228284 conn .commit ()
229285 pid = cur .lastrowid
230286 conn .close ()
287+ try :
288+ record_audit ('project.create' , actor_user_id = None , details = f'project={ name } , repo={ repo_url } ' )
289+ except Exception :
290+ pass
231291 return pid
232292
233293
@@ -285,13 +345,39 @@ def list_milestones(project_id: int):
285345 return [dict (r ) for r in rows ]
286346
287347
348+ def add_project_member (project_id : int , user_id : int , role : str = 'member' ):
349+ conn = get_connection ()
350+ cur = conn .cursor ()
351+ cur .execute ('INSERT INTO project_members (project_id, user_id, role) VALUES (?, ?, ?)' , (project_id , user_id , role ))
352+ conn .commit ()
353+ conn .close ()
354+ try :
355+ record_audit ('project.member.add' , actor_user_id = None , details = f'project_id={ project_id } , user_id={ user_id } , role={ role } ' )
356+ except Exception :
357+ pass
358+ return True
359+
360+
361+ def list_project_members (project_id : int ):
362+ conn = get_connection ()
363+ cur = conn .cursor ()
364+ cur .execute ('SELECT pm.id, pm.user_id, pm.role, u.email, u.full_name, pm.added_at FROM project_members pm LEFT JOIN users u ON u.id = pm.user_id WHERE pm.project_id = ? ORDER BY pm.added_at DESC' , (project_id ,))
365+ rows = cur .fetchall ()
366+ conn .close ()
367+ return [dict (r ) for r in rows ]
368+
369+
288370def create_group (name : str , description : str = None ):
289371 conn = get_connection ()
290372 cur = conn .cursor ()
291373 cur .execute ('INSERT INTO groups (name, description) VALUES (?, ?)' , (name , description ))
292374 conn .commit ()
293375 gid = cur .lastrowid
294376 conn .close ()
377+ try :
378+ record_audit ('group.create' , actor_user_id = None , details = f'group={ name } ' )
379+ except Exception :
380+ pass
295381 return gid
296382
297383
@@ -346,6 +432,10 @@ def record_upload(user_id: int, filename: str, status: str, message: str = None,
346432 cur .execute ('INSERT INTO uploads (user_id, filename, status, message, chunks_indexed) VALUES (?, ?, ?, ?, ?)' , (user_id , filename , status , message , chunks_indexed ))
347433 conn .commit ()
348434 conn .close ()
435+ try :
436+ record_audit ('upload.record' , actor_user_id = user_id , details = f'{ filename } status={ status } msg={ message } ' )
437+ except Exception :
438+ pass
349439
350440
351441def list_uploads_for_user (user_id : int , limit : int = 20 ):
@@ -366,6 +456,10 @@ def create_api_token(user_id: int, name: str, token_plain: str):
366456 cur .execute ('INSERT INTO api_tokens (user_id, name, token_hash) VALUES (?, ?, ?)' , (user_id , name , token_hash ))
367457 conn .commit ()
368458 conn .close ()
459+ try :
460+ record_audit ('api.token.create' , actor_user_id = user_id , details = f'name={ name } ' )
461+ except Exception :
462+ pass
369463 return True
370464
371465
@@ -388,6 +482,10 @@ def revoke_api_token(token_id: int, user_id: int):
388482 cur .execute ('UPDATE api_tokens SET revoked_at = CURRENT_TIMESTAMP WHERE id = ?' , (token_id ,))
389483 conn .commit ()
390484 conn .close ()
485+ try :
486+ record_audit ('api.token.revoke' , actor_user_id = user_id , details = f'token_id={ token_id } ' )
487+ except Exception :
488+ pass
391489 return True
392490
393491
@@ -399,6 +497,10 @@ def create_user(email: str, password: str) -> bool:
399497 cur = conn .cursor ()
400498 cur .execute ('INSERT INTO users (email, password) VALUES (?, ?)' , (email , pw_hash ))
401499 conn .commit ()
500+ try :
501+ record_audit ('user.create' , actor_user_id = None , details = f'email={ email } ' )
502+ except Exception :
503+ pass
402504 return True
403505 except sqlite3 .IntegrityError :
404506 return False
0 commit comments