Skip to main content

agent_relay/
hub.rs

1//! Hub: auth, teams, channels, and invite logic for agent-relay.
2
3use rusqlite::Connection;
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6
7// ── Data Types ──
8
9#[derive(Serialize, Deserialize, Clone, Debug)]
10pub struct User {
11    pub id: String,
12    pub email: String,
13    pub name: String,
14}
15
16#[derive(Serialize, Deserialize, Clone, Debug)]
17pub struct Team {
18    pub id: String,
19    pub name: String,
20    pub owner_id: String,
21    pub created_at: u64,
22}
23
24#[derive(Serialize, Deserialize, Clone, Debug)]
25pub struct TeamMember {
26    pub user_id: String,
27    pub role: String,
28    pub joined_at: u64,
29    pub name: String,
30    pub email: String,
31}
32
33#[derive(Serialize, Deserialize, Clone, Debug)]
34pub struct Invite {
35    pub token: String,
36    pub team_id: String,
37    pub team_name: String,
38}
39
40#[derive(Serialize, Deserialize, Clone, Debug)]
41pub struct Channel {
42    pub id: String,
43    pub name: String,
44    pub team_id: String,
45}
46
47// ── Schema ──
48
49/// Create all hub tables and migrate the messages table.
50pub fn init_hub_tables(conn: &Connection) {
51    conn.execute_batch(
52        "CREATE TABLE IF NOT EXISTS users (
53            id TEXT PRIMARY KEY,
54            email TEXT UNIQUE NOT NULL,
55            name TEXT NOT NULL,
56            created_at INTEGER NOT NULL
57        );
58
59        CREATE TABLE IF NOT EXISTS api_keys (
60            key_hash TEXT PRIMARY KEY,
61            user_id TEXT NOT NULL REFERENCES users(id),
62            label TEXT NOT NULL DEFAULT 'default',
63            created_at INTEGER NOT NULL
64        );
65
66        CREATE TABLE IF NOT EXISTS teams (
67            id TEXT PRIMARY KEY,
68            name TEXT UNIQUE NOT NULL,
69            owner_id TEXT NOT NULL REFERENCES users(id),
70            created_at INTEGER NOT NULL
71        );
72
73        CREATE TABLE IF NOT EXISTS team_members (
74            team_id TEXT NOT NULL REFERENCES teams(id),
75            user_id TEXT NOT NULL REFERENCES users(id),
76            role TEXT NOT NULL DEFAULT 'member',
77            joined_at INTEGER NOT NULL,
78            PRIMARY KEY (team_id, user_id)
79        );
80
81        CREATE TABLE IF NOT EXISTS invites (
82            token TEXT PRIMARY KEY,
83            team_id TEXT NOT NULL REFERENCES teams(id),
84            inviter_id TEXT NOT NULL REFERENCES users(id),
85            email TEXT,
86            created_at INTEGER NOT NULL,
87            used_at INTEGER
88        );
89
90        CREATE TABLE IF NOT EXISTS channels (
91            id TEXT PRIMARY KEY,
92            team_id TEXT NOT NULL REFERENCES teams(id),
93            name TEXT NOT NULL,
94            created_at INTEGER NOT NULL,
95            UNIQUE(team_id, name)
96        );",
97    )
98    .expect("Failed to create hub tables");
99
100    // Migrate messages table: add team_id and channel columns if missing.
101    // SQLite ALTER TABLE ADD COLUMN fails if column exists, so we catch the error.
102    let _ = conn.execute_batch("ALTER TABLE messages ADD COLUMN team_id TEXT;");
103    let _ = conn.execute_batch("ALTER TABLE messages ADD COLUMN channel TEXT DEFAULT 'general';");
104}
105
106// ── API Key Helpers ──
107
108/// SHA256 hash of an API key for secure storage.
109pub fn hash_api_key(key: &str) -> String {
110    let mut hasher = Sha256::new();
111    hasher.update(key.as_bytes());
112    hex::encode(hasher.finalize())
113}
114
115/// Generate a new API key with the `ar_` prefix.
116pub fn generate_api_key() -> String {
117    format!("ar_{}", uuid::Uuid::new_v4().to_string().replace('-', ""))
118}
119
120/// Look up an API key, return the associated user if valid.
121pub fn verify_api_key(conn: &Connection, key: &str) -> Option<User> {
122    let key_hash = hash_api_key(key);
123    conn.query_row(
124        "SELECT u.id, u.email, u.name
125         FROM api_keys ak
126         JOIN users u ON u.id = ak.user_id
127         WHERE ak.key_hash = ?1",
128        [&key_hash],
129        |row| {
130            Ok(User {
131                id: row.get(0)?,
132                email: row.get(1)?,
133                name: row.get(2)?,
134            })
135        },
136    )
137    .ok()
138}