Skip to content

Commit 8551a93

Browse files
committed
commit f1c2d3e4g5h6i7j8k9l0m1n2o3p4q5r6s7t8u9v0
1 parent ad3458f commit 8551a93

File tree

8 files changed

+417
-1
lines changed

8 files changed

+417
-1
lines changed

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,63 @@ For production, use the provided `Dockerfile` and `compose.yaml` as a basis and
148148

149149
---
150150

151+
## Admin & audit logs
152+
153+
This project includes a simple admin console and audit logging to help administrators monitor important events.
154+
155+
- Make a user an admin:
156+
- The `users` table contains an `is_admin` column (0/1). To grant admin rights to an existing user run:
157+
158+
```sql
159+
UPDATE dbkamp.sqlite3 SET is_admin = 1 WHERE email = 'admin@example.com';
160+
```
161+
162+
- Or, using the Python DB helpers, you can set the field manually in a script that connects via `dbkamp.db.get_connection()`.
163+
164+
- Viewing audit logs:
165+
- Admins can visit `/dashboard/admin` in the app to inspect recent audit events (sign-ins, token creation/revocation, group/project changes, uploads, etc.).
166+
- Audit events are stored in the `audit_logs` table (columns: `event_type`, `actor_user_id`, `details`, `ip`, `created_at`).
167+
168+
- Notes and next steps:
169+
- The admin console is read-only by default; actions such as revoking sessions or rotating tokens are UI placeholders and require backend handlers.
170+
- For production, forward audit logs to a centralized logging system (ELK / Splunk / Cloud Logging) and enable secure retention and access controls.
171+
172+
### How super users (admins) log in
173+
174+
- Admins use the normal login flow (email/password or OAuth). Once a user's `is_admin` flag is set to `1`, they can visit `/dashboard/admin` to access the admin console.
175+
- Use the provided convenience script to promote a user by email:
176+
177+
```bash
178+
./scripts/promote_user_to_admin.py admin@example.com
179+
```
180+
181+
After promotion, the user simply logs in and navigates to `/dashboard/admin`.
182+
183+
---
184+
151185
## Contributing
152186

153187
Contributions are welcome. Please open PRs against `main` and include a short description and tests where possible.
154188

155189

190+
---
191+
192+
## Text generation & TTS (optional)
193+
194+
This project includes optional endpoints for text generation and text-to-speech. These are disabled until you install the required packages.
195+
196+
Install dependencies (recommended inside your virtualenv):
197+
198+
```bash
199+
pip install transformers torch TTS[all] soundfile numpy
200+
```
201+
202+
Endpoints:
203+
- POST `/api/generate` JSON {"prompt": "Hello"} → returns generated text
204+
- POST `/api/tts` JSON {"text": "Hello world"} → returns WAV audio
205+
206+
Notes:
207+
- The code uses lazy imports and will return a helpful error if the dependencies are missing.
208+
- First run may download models (this can be large). For GPU acceleration, install a CUDA-enabled `torch` build.
209+
210+

dbkamp/db.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

5760
def 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

160177
def 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+
185219
def 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+
224280
def 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+
288370
def 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

351441
def 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

models/tts_and_text.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import io
2+
import logging
3+
4+
logger = logging.getLogger(__name__)
5+
6+
_text_gen = None
7+
_tts = None
8+
9+
def text_generate(prompt: str, model_name: str = 'gpt2', max_new_tokens: int = 128):
10+
"""Generate text from a prompt using Hugging Face pipelines (lazy import).
11+
12+
Returns generated text on success or raises ImportError if transformers not installed.
13+
"""
14+
global _text_gen
15+
try:
16+
from transformers import pipeline
17+
except Exception as e:
18+
logger.exception('transformers not available')
19+
raise ImportError('transformers package is required for text generation') from e
20+
21+
if _text_gen is None:
22+
_text_gen = pipeline('text-generation', model=model_name)
23+
out = _text_gen(prompt, max_new_tokens=max_new_tokens, do_sample=True, top_p=0.95)
24+
return out[0].get('generated_text')
25+
26+
27+
def synthesize_speech(text: str, tts_model: str = 'tts_models/en/ljspeech/tacotron2-DDC'):
28+
"""Synthesize speech using Coqui TTS (lazy import).
29+
30+
Returns raw WAV bytes. Raises ImportError if TTS not available.
31+
"""
32+
global _tts
33+
try:
34+
from TTS.api import TTS
35+
import soundfile as sf
36+
import numpy as np
37+
except Exception as e:
38+
logger.exception('TTS dependencies not available')
39+
raise ImportError('Coqui TTS and soundfile packages are required for TTS') from e
40+
41+
if _tts is None:
42+
_tts = TTS(tts_model)
43+
44+
# TTS.tts returns a numpy array of audio samples
45+
wav = _tts.tts(text)
46+
sample_rate = _tts.synthesizer.output_sample_rate if hasattr(_tts, 'synthesizer') else 22050
47+
48+
buf = io.BytesIO()
49+
sf.write(buf, wav.astype(np.float32), sample_rate, format='WAV')
50+
buf.seek(0)
51+
return buf.read()
52+
# models/tts_and_text.py
53+
import io
54+
import soundfile as sf
55+
from transformers import pipeline
56+
from TTS.api import TTS
57+
58+
# text generation (small example)
59+
_text_gen = None
60+
def text_generate(prompt, model_name="gpt2"):
61+
global _text_gen
62+
if _text_gen is None:
63+
_text_gen = pipeline("text-generation", model=model_name)
64+
out = _text_gen(prompt, max_new_tokens=128, do_sample=True, top_p=0.95)
65+
return out[0]['generated_text']
66+
67+
# TTS using Coqui TTS (will download prebuilt model on first run)
68+
_tts = None
69+
def synthesize_speech(text, tts_model="tts_models/en/ljspeech/tacotron2-DDC"):
70+
global _tts
71+
if _tts is None:
72+
_tts = TTS(tts_model) # choose a model id from Coqui
73+
wav = _tts.tts(text)
74+
# return raw bytes (WAV)
75+
buf = io.BytesIO()
76+
sf.write(buf, wav, _tts.synthesizer.output_sample_rate, format='WAV')
77+
return buf.getvalue()

requirements.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,9 @@ black
2828
isort
2929
flake8
3030
sentence-transformers
31-
faiss-cpu
31+
faiss-cpu
32+
transformers
33+
torch
34+
TTS[all]
35+
soundfile
36+
numpy

0 commit comments

Comments
 (0)