Add API tokens, CLI server API and shared core crate
- Personal API tokens (full/read scope, expiry, revoke) and CLI password login - Cargo workspace: codecraft-core (types, providers, permission rules) shared by server and the upcoming terminal client; server re-exports the old paths - CLI server API: /cli/config, /cli/skill, /cli/tool (server MCP tools), /llm/stream model gateway with budgets and usage per user - Theme/language preferences mirrored to the server; personal memory endpoint - Statistics: provider and mode tables stacked; CLI usage labelled - cli/: work in progress (terminal client is being rewritten as a local TUI) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fca1c41087
commit
aa1f8cf016
40 files changed
+2454
-814
No files matched your search
+69
-3
@@ -97,6 +97,17 @@ impl Store {
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS queue_session ON queue(session_id, created_at);
|
||||
CREATE TABLE IF NOT EXISTS api_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
prefix TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_used INTEGER,
|
||||
expires_at INTEGER,
|
||||
scope TEXT NOT NULL DEFAULT 'full'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner_id TEXT NOT NULL,
|
||||
@@ -317,6 +328,8 @@ pub struct UsageRecord {
|
||||
pub duration_ms: u64,
|
||||
pub cost: f64,
|
||||
pub error: bool,
|
||||
/// User to bill when there is no session (CLI requests); else the session owner.
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Aggregation SQL shared by all statistics queries.
|
||||
@@ -364,11 +377,11 @@ impl Store {
|
||||
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
||||
ttft_ms, duration_ms, cost, error, user_id)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,
|
||||
(SELECT owner_id FROM sessions WHERE id = ?3))",
|
||||
COALESCE(?15, (SELECT owner_id FROM sessions WHERE id = ?3)))",
|
||||
params![
|
||||
u.ts, u.session_id, u.root_session_id, u.provider, u.model, u.agent,
|
||||
u.input_tokens as i64, u.output_tokens as i64, u.cache_read_tokens as i64,
|
||||
u.cache_write_tokens as i64, u.ttft_ms as i64, u.duration_ms as i64, u.cost, u.error as i64
|
||||
u.cache_write_tokens as i64, u.ttft_ms as i64, u.duration_ms as i64, u.cost, u.error as i64, u.user_id
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -474,7 +487,7 @@ impl Store {
|
||||
// Most expensive chats in the range.
|
||||
let top_sessions = {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT u.root_session_id, COALESCE(json_extract(s.data, '$.title'), '(deleted)'), {AGG}
|
||||
"SELECT u.root_session_id, COALESCE(json_extract(s.data, '$.title'), CASE WHEN u.root_session_id LIKE 'cli:%' THEN 'CLI (local)' ELSE '(deleted)' END), {AGG}
|
||||
FROM usage u LEFT JOIN sessions s ON s.id = u.root_session_id
|
||||
WHERE u.ts >= ?1 AND u.ts < ?2 AND (?3 IS NULL OR u.user_id = ?3)
|
||||
GROUP BY 1 ORDER BY SUM(u.cost) DESC, COUNT(*) DESC LIMIT 15"
|
||||
@@ -623,6 +636,59 @@ impl Store {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- personal API tokens ------------------------------------------------
|
||||
|
||||
pub fn api_token_add(&self, user_id: &str, name: &str, token_hash: &str, prefix: &str, expires_at: Option<i64>, scope: &str) -> Result<String> {
|
||||
let id = crate::types::new_id();
|
||||
self.conn.lock().unwrap().execute(
|
||||
"INSERT INTO api_tokens (id, user_id, name, token_hash, prefix, created_at, expires_at, scope) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
params![id, user_id, name, token_hash, prefix, now_ms(), expires_at, scope],
|
||||
)?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn api_tokens(&self, user_id: &str) -> Result<Vec<serde_json::Value>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare("SELECT id, name, prefix, created_at, last_used, expires_at, scope FROM api_tokens WHERE user_id = ?1 ORDER BY created_at")?;
|
||||
let rows = stmt.query_map([user_id], |r| {
|
||||
Ok(serde_json::json!({
|
||||
"id": r.get::<_, String>(0)?, "name": r.get::<_, String>(1)?, "prefix": r.get::<_, String>(2)?,
|
||||
"created_at": r.get::<_, i64>(3)?, "last_used": r.get::<_, Option<i64>>(4)?, "expires_at": r.get::<_, Option<i64>>(5)?, "scope": r.get::<_, String>(6)?,
|
||||
}))
|
||||
})?;
|
||||
Ok(rows.collect::<std::result::Result<_, _>>()?)
|
||||
}
|
||||
|
||||
pub fn api_token_delete(&self, user_id: &str, id: &str) -> Result<bool> {
|
||||
Ok(self.conn.lock().unwrap().execute("DELETE FROM api_tokens WHERE id = ?1 AND user_id = ?2", [id, user_id])? > 0)
|
||||
}
|
||||
|
||||
/// Scope ("full" / "read") of a personal API token.
|
||||
pub fn api_token_scope(&self, token_hash: &str) -> Option<String> {
|
||||
self.conn.lock().unwrap().query_row("SELECT scope FROM api_tokens WHERE token_hash = ?1", [token_hash], |r| r.get(0)).optional().ok().flatten()
|
||||
}
|
||||
|
||||
/// The (enabled) user behind a valid personal API token.
|
||||
pub fn user_by_api_token(&self, token_hash: &str) -> Result<Option<User>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let user = conn
|
||||
.query_row(
|
||||
&format!(
|
||||
"SELECT {} FROM api_tokens t JOIN users u ON u.id = t.user_id
|
||||
WHERE t.token_hash = ?1 AND (t.expires_at IS NULL OR t.expires_at > ?2) AND u.disabled = 0",
|
||||
USER_COLS.split(", ").map(|c| format!("u.{c}")).collect::<Vec<_>>().join(", ")
|
||||
),
|
||||
params![token_hash, now_ms()],
|
||||
user_row,
|
||||
)
|
||||
.optional()?;
|
||||
if user.is_some() {
|
||||
// At most one write per minute per token.
|
||||
conn.execute("UPDATE api_tokens SET last_used = ?2 WHERE token_hash = ?1 AND (last_used IS NULL OR last_used < ?2 - 60000)", params![token_hash, now_ms()])?;
|
||||
}
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// The (enabled) user behind a valid login token.
|
||||
pub fn user_by_token(&self, token_hash: &str) -> Result<Option<User>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
Reference in new issue
Block a user