1use axum::{
9 extract::{Path as AxumPath, Query, State},
10 http::{HeaderMap, StatusCode},
11 routing::{get, post},
12 Json, Router,
13};
14use rusqlite::Connection;
15use serde::{Deserialize, Serialize};
16use std::path::Path;
17use std::sync::{Arc, Mutex};
18
19use crate::hub;
20use crate::{AgentRegistration, Message};
21
22pub type SharedState = Arc<Mutex<Connection>>;
25
26pub fn init_db(conn: &Connection) {
28 conn.execute_batch(
29 "CREATE TABLE IF NOT EXISTS messages (
30 id TEXT PRIMARY KEY,
31 from_session TEXT NOT NULL,
32 from_agent TEXT NOT NULL,
33 to_session TEXT,
34 content TEXT NOT NULL,
35 timestamp INTEGER NOT NULL
36 );
37 CREATE TABLE IF NOT EXISTS message_reads (
38 message_id TEXT NOT NULL,
39 session_id TEXT NOT NULL,
40 PRIMARY KEY (message_id, session_id)
41 );
42 CREATE TABLE IF NOT EXISTS agents (
43 session_id TEXT PRIMARY KEY,
44 agent_id TEXT NOT NULL,
45 pid INTEGER NOT NULL,
46 registered_at INTEGER NOT NULL,
47 last_heartbeat INTEGER NOT NULL,
48 metadata TEXT NOT NULL DEFAULT '{}'
49 );",
50 )
51 .expect("Failed to initialize database schema");
52
53 hub::init_hub_tables(conn);
54}
55
56#[derive(Deserialize)]
59pub struct SendRequest {
60 pub from_session: String,
61 pub from_agent: String,
62 pub to_session: Option<String>,
63 pub content: String,
64 pub team_id: Option<String>,
65 pub channel: Option<String>,
66}
67
68#[derive(Deserialize)]
69pub struct InboxQuery {
70 pub session: String,
71 #[serde(default = "default_limit")]
72 pub limit: usize,
73 pub team: Option<String>,
74 pub channel: Option<String>,
75}
76
77fn default_limit() -> usize {
78 50
79}
80
81#[derive(Deserialize)]
82pub struct RegisterRequest {
83 pub session_id: String,
84 pub agent_id: String,
85 pub pid: u32,
86 #[serde(default)]
87 pub metadata: serde_json::Value,
88}
89
90#[derive(Serialize)]
91pub struct CountResponse {
92 pub count: u64,
93}
94
95pub fn build_router(db_path: &Path) -> Router {
99 let conn = Connection::open(db_path).expect("Failed to open SQLite database");
100 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;")
101 .expect("Failed to set pragmas");
102 init_db(&conn);
103
104 let state: SharedState = Arc::new(Mutex::new(conn));
105
106 Router::new()
107 .route("/health", get(health))
108 .route("/agents", get(list_agents))
109 .route("/agents/register", post(register_agent))
110 .route("/agents/unregister", post(unregister_agent))
111 .route("/messages/send", post(send_message))
112 .route("/messages/inbox", get(inbox))
113 .route("/messages/unread", get(unread_count))
114 .route("/auth/signup", post(signup))
116 .route("/auth/login", post(login))
117 .route("/teams", post(create_team).get(list_teams))
119 .route("/teams/join", post(join_team))
120 .route("/teams/{team_id}/invite", post(create_invite))
121 .route("/teams/{team_id}/members", get(list_members))
122 .route("/teams/{team_id}/kick", post(kick_member))
123 .route(
125 "/teams/{team_id}/channels",
126 post(create_channel).get(list_channels),
127 )
128 .layer(
129 tower_http::cors::CorsLayer::new()
130 .allow_origin(tower_http::cors::Any)
131 .allow_methods([
132 axum::http::Method::GET,
133 axum::http::Method::POST,
134 ])
135 .allow_headers([axum::http::header::CONTENT_TYPE, axum::http::header::AUTHORIZATION])
136 .max_age(std::time::Duration::from_secs(3600)),
137 )
138 .with_state(state)
139}
140
141async fn health() -> &'static str {
144 "agent-relay server ok"
145}
146
147async fn register_agent(
148 State(state): State<SharedState>,
149 Json(req): Json<RegisterRequest>,
150) -> (StatusCode, Json<AgentRegistration>) {
151 let now = crate::Relay::now();
152 let metadata_str = req.metadata.to_string();
153
154 let conn = state.lock().unwrap();
155 conn.execute(
156 "INSERT OR REPLACE INTO agents (session_id, agent_id, pid, registered_at, last_heartbeat, metadata)
157 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
158 rusqlite::params![req.session_id, req.agent_id, req.pid, now, now, metadata_str],
159 )
160 .expect("Failed to insert agent");
161
162 let reg = AgentRegistration {
163 session_id: req.session_id,
164 agent_id: req.agent_id,
165 pid: req.pid,
166 registered_at: now,
167 last_heartbeat: now,
168 metadata: req.metadata,
169 };
170
171 (StatusCode::CREATED, Json(reg))
172}
173
174async fn unregister_agent(
175 State(state): State<SharedState>,
176 Json(req): Json<serde_json::Value>,
177) -> StatusCode {
178 let session = req["session_id"].as_str().unwrap_or("");
179 let conn = state.lock().unwrap();
180 let _ = conn.execute("DELETE FROM agents WHERE session_id = ?1", [session]);
181 StatusCode::OK
182}
183
184async fn list_agents(State(state): State<SharedState>) -> Json<Vec<AgentRegistration>> {
185 let conn = state.lock().unwrap();
186 let mut stmt = conn
187 .prepare("SELECT session_id, agent_id, pid, registered_at, last_heartbeat, metadata FROM agents ORDER BY last_heartbeat DESC")
188 .unwrap();
189
190 let agents: Vec<AgentRegistration> = stmt
191 .query_map([], |row| {
192 let metadata_str: String = row.get(5)?;
193 Ok(AgentRegistration {
194 session_id: row.get(0)?,
195 agent_id: row.get(1)?,
196 pid: row.get::<_, u32>(2)?,
197 registered_at: row.get(3)?,
198 last_heartbeat: row.get(4)?,
199 metadata: serde_json::from_str(&metadata_str).unwrap_or_default(),
200 })
201 })
202 .unwrap()
203 .filter_map(|r| r.ok())
204 .collect();
205
206 Json(agents)
207}
208
209async fn send_message(
210 State(state): State<SharedState>,
211 Json(req): Json<SendRequest>,
212) -> Result<(StatusCode, Json<Message>), (StatusCode, String)> {
213 if req.content.len() > 10_000 {
215 return Err((
216 StatusCode::PAYLOAD_TOO_LARGE,
217 "Message content exceeds 10KB limit".to_string(),
218 ));
219 }
220 if req.content.is_empty() {
221 return Err((
222 StatusCode::BAD_REQUEST,
223 "Message content cannot be empty".to_string(),
224 ));
225 }
226
227 let now = crate::Relay::now();
228 let id = format!("msg-{}", &uuid::Uuid::new_v4().to_string()[..8]);
229
230 let conn = state.lock().unwrap();
231 conn.execute(
232 "INSERT INTO messages (id, from_session, from_agent, to_session, content, timestamp, team_id, channel)
233 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
234 rusqlite::params![
235 id,
236 req.from_session,
237 req.from_agent,
238 req.to_session,
239 req.content,
240 now,
241 req.team_id,
242 req.channel.as_deref().unwrap_or("general")
243 ],
244 )
245 .expect("Failed to insert message");
246
247 let _ = conn.execute(
249 "INSERT OR IGNORE INTO message_reads (message_id, session_id) VALUES (?1, ?2)",
250 rusqlite::params![id, req.from_session],
251 );
252
253 let msg = Message {
254 id,
255 from_session: req.from_session,
256 from_agent: req.from_agent,
257 to_session: req.to_session,
258 content: req.content,
259 timestamp: now,
260 read_by: vec![],
261 };
262
263 Ok((StatusCode::CREATED, Json(msg)))
264}
265
266async fn inbox(
267 State(state): State<SharedState>,
268 Query(q): Query<InboxQuery>,
269) -> Json<Vec<Message>> {
270 let conn = state.lock().unwrap();
271
272 let mut sql = String::from(
273 "SELECT id, from_session, from_agent, to_session, content, timestamp
274 FROM messages
275 WHERE (to_session IS NULL OR to_session = ?1 OR from_session = ?1)",
276 );
277 let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = vec![
278 Box::new(q.session.clone()),
279 ];
280
281 if let Some(ref team) = q.team {
282 params.push(Box::new(team.clone()));
283 sql.push_str(&format!(" AND team_id = ?{}", params.len()));
284 }
285 if let Some(ref channel) = q.channel {
286 params.push(Box::new(channel.clone()));
287 sql.push_str(&format!(" AND channel = ?{}", params.len()));
288 }
289
290 params.push(Box::new(q.limit as i64));
291 sql.push_str(&format!(" ORDER BY timestamp DESC LIMIT ?{}", params.len()));
292
293 let mut stmt = conn.prepare(&sql).unwrap();
294 let param_refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect();
295
296 let messages: Vec<Message> = stmt
297 .query_map(param_refs.as_slice(), |row| {
298 Ok(Message {
299 id: row.get(0)?,
300 from_session: row.get(1)?,
301 from_agent: row.get(2)?,
302 to_session: row.get(3)?,
303 content: row.get(4)?,
304 timestamp: row.get(5)?,
305 read_by: vec![],
306 })
307 })
308 .unwrap()
309 .filter_map(|r| r.ok())
310 .collect();
311
312 for msg in &messages {
314 let _ = conn.execute(
315 "INSERT OR IGNORE INTO message_reads (message_id, session_id) VALUES (?1, ?2)",
316 rusqlite::params![msg.id, q.session],
317 );
318 }
319
320 Json(messages)
321}
322
323async fn unread_count(
324 State(state): State<SharedState>,
325 Query(q): Query<InboxQuery>,
326) -> Json<CountResponse> {
327 let conn = state.lock().unwrap();
328
329 let count: u64 = conn
330 .query_row(
331 "SELECT COUNT(*) FROM messages m
332 WHERE (m.to_session IS NULL OR m.to_session = ?1)
333 AND m.from_session != ?1
334 AND NOT EXISTS (
335 SELECT 1 FROM message_reads mr
336 WHERE mr.message_id = m.id AND mr.session_id = ?1
337 )",
338 [&q.session],
339 |row| row.get(0),
340 )
341 .unwrap_or(0);
342
343 Json(CountResponse { count })
344}
345
346fn extract_user(
349 state: &SharedState,
350 headers: &HeaderMap,
351) -> Result<hub::User, (StatusCode, String)> {
352 let auth = headers
353 .get("authorization")
354 .and_then(|v| v.to_str().ok())
355 .and_then(|v| v.strip_prefix("Bearer "))
356 .ok_or((
357 StatusCode::UNAUTHORIZED,
358 "Missing Authorization header".to_string(),
359 ))?;
360 let conn = state.lock().unwrap();
361 hub::verify_api_key(&conn, auth).ok_or((
362 StatusCode::UNAUTHORIZED,
363 "Invalid API key".to_string(),
364 ))
365}
366
367#[derive(Deserialize)]
370struct SignupRequest {
371 email: String,
372 name: String,
373}
374
375#[derive(Serialize)]
376struct SignupResponse {
377 user: hub::User,
378 api_key: String,
379}
380
381#[derive(Deserialize)]
382struct LoginRequest {
383 email: String,
384}
385
386#[derive(Serialize)]
387struct LoginResponse {
388 api_key: String,
389}
390
391async fn signup(
394 State(state): State<SharedState>,
395 Json(req): Json<SignupRequest>,
396) -> Result<(StatusCode, Json<SignupResponse>), (StatusCode, String)> {
397 let conn = state.lock().unwrap();
398 let now = crate::Relay::now();
399 let user_id = uuid::Uuid::new_v4().to_string();
400
401 conn.execute(
402 "INSERT INTO users (id, email, name, created_at) VALUES (?1, ?2, ?3, ?4)",
403 rusqlite::params![user_id, req.email, req.name, now],
404 )
405 .map_err(|e| {
406 if e.to_string().contains("UNIQUE") {
407 (
408 StatusCode::CONFLICT,
409 format!("Email '{}' already registered. Use /auth/login instead.", req.email),
410 )
411 } else {
412 (
413 StatusCode::INTERNAL_SERVER_ERROR,
414 format!("Database error: {}", e),
415 )
416 }
417 })?;
418
419 let api_key = hub::generate_api_key();
420 let key_hash = hub::hash_api_key(&api_key);
421
422 conn.execute(
423 "INSERT INTO api_keys (key_hash, user_id, label, created_at) VALUES (?1, ?2, ?3, ?4)",
424 rusqlite::params![key_hash, user_id, "default", now],
425 )
426 .map_err(|e| {
427 (
428 StatusCode::INTERNAL_SERVER_ERROR,
429 format!("Failed to create API key: {}", e),
430 )
431 })?;
432
433 let user = hub::User {
434 id: user_id,
435 email: req.email,
436 name: req.name,
437 };
438
439 Ok((
440 StatusCode::CREATED,
441 Json(SignupResponse {
442 user,
443 api_key,
444 }),
445 ))
446}
447
448async fn login(
449 State(state): State<SharedState>,
450 Json(req): Json<LoginRequest>,
451) -> Result<Json<LoginResponse>, (StatusCode, String)> {
452 let conn = state.lock().unwrap();
453 let now = crate::Relay::now();
454
455 let user_id: String = conn
456 .query_row(
457 "SELECT id FROM users WHERE email = ?1",
458 [&req.email],
459 |row| row.get(0),
460 )
461 .map_err(|_| {
462 (
463 StatusCode::NOT_FOUND,
464 format!("No account found for '{}'", req.email),
465 )
466 })?;
467
468 let api_key = hub::generate_api_key();
469 let key_hash = hub::hash_api_key(&api_key);
470
471 conn.execute(
472 "INSERT INTO api_keys (key_hash, user_id, label, created_at) VALUES (?1, ?2, ?3, ?4)",
473 rusqlite::params![key_hash, user_id, "default", now],
474 )
475 .map_err(|e| {
476 (
477 StatusCode::INTERNAL_SERVER_ERROR,
478 format!("Failed to create API key: {}", e),
479 )
480 })?;
481
482 Ok(Json(LoginResponse { api_key }))
483}
484
485#[derive(Deserialize)]
488struct CreateTeamRequest {
489 name: String,
490}
491
492#[derive(Deserialize)]
493struct InviteRequest {
494 email: Option<String>,
495}
496
497#[derive(Serialize)]
498struct InviteResponse {
499 token: String,
500}
501
502#[derive(Deserialize)]
503struct JoinTeamRequest {
504 token: String,
505}
506
507#[derive(Deserialize)]
508struct KickRequest {
509 user_id: String,
510}
511
512#[derive(Deserialize)]
513struct CreateChannelRequest {
514 name: String,
515}
516
517async fn create_team(
520 State(state): State<SharedState>,
521 headers: HeaderMap,
522 Json(req): Json<CreateTeamRequest>,
523) -> Result<(StatusCode, Json<hub::Team>), (StatusCode, String)> {
524 let user = extract_user(&state, &headers)?;
525 let conn = state.lock().unwrap();
526 let now = crate::Relay::now();
527 let team_id = uuid::Uuid::new_v4().to_string();
528
529 conn.execute(
530 "INSERT INTO teams (id, name, owner_id, created_at) VALUES (?1, ?2, ?3, ?4)",
531 rusqlite::params![team_id, req.name, user.id, now],
532 )
533 .map_err(|e| {
534 if e.to_string().contains("UNIQUE") {
535 (
536 StatusCode::CONFLICT,
537 format!("Team name '{}' already taken", req.name),
538 )
539 } else {
540 (
541 StatusCode::INTERNAL_SERVER_ERROR,
542 format!("Database error: {}", e),
543 )
544 }
545 })?;
546
547 conn.execute(
549 "INSERT INTO team_members (team_id, user_id, role, joined_at) VALUES (?1, ?2, ?3, ?4)",
550 rusqlite::params![team_id, user.id, "owner", now],
551 )
552 .map_err(|e| {
553 (
554 StatusCode::INTERNAL_SERVER_ERROR,
555 format!("Failed to add owner: {}", e),
556 )
557 })?;
558
559 let channel_id = uuid::Uuid::new_v4().to_string();
561 conn.execute(
562 "INSERT INTO channels (id, team_id, name, created_at) VALUES (?1, ?2, ?3, ?4)",
563 rusqlite::params![channel_id, team_id, "general", now],
564 )
565 .map_err(|e| {
566 (
567 StatusCode::INTERNAL_SERVER_ERROR,
568 format!("Failed to create #general channel: {}", e),
569 )
570 })?;
571
572 let team = hub::Team {
573 id: team_id,
574 name: req.name,
575 owner_id: user.id,
576 created_at: now,
577 };
578
579 Ok((StatusCode::CREATED, Json(team)))
580}
581
582async fn list_teams(
583 State(state): State<SharedState>,
584 headers: HeaderMap,
585) -> Result<Json<Vec<hub::Team>>, (StatusCode, String)> {
586 let user = extract_user(&state, &headers)?;
587 let conn = state.lock().unwrap();
588
589 let mut stmt = conn
590 .prepare(
591 "SELECT t.id, t.name, t.owner_id, t.created_at
592 FROM teams t
593 JOIN team_members tm ON tm.team_id = t.id
594 WHERE tm.user_id = ?1
595 ORDER BY t.created_at DESC",
596 )
597 .map_err(|e| {
598 (
599 StatusCode::INTERNAL_SERVER_ERROR,
600 format!("Query error: {}", e),
601 )
602 })?;
603
604 let teams: Vec<hub::Team> = stmt
605 .query_map([&user.id], |row| {
606 Ok(hub::Team {
607 id: row.get(0)?,
608 name: row.get(1)?,
609 owner_id: row.get(2)?,
610 created_at: row.get(3)?,
611 })
612 })
613 .map_err(|e| {
614 (
615 StatusCode::INTERNAL_SERVER_ERROR,
616 format!("Query error: {}", e),
617 )
618 })?
619 .filter_map(|r| r.ok())
620 .collect();
621
622 Ok(Json(teams))
623}
624
625async fn create_invite(
626 State(state): State<SharedState>,
627 headers: HeaderMap,
628 AxumPath(team_id): AxumPath<String>,
629 Json(req): Json<InviteRequest>,
630) -> Result<(StatusCode, Json<InviteResponse>), (StatusCode, String)> {
631 let user = extract_user(&state, &headers)?;
632 let conn = state.lock().unwrap();
633
634 let role: String = conn
636 .query_row(
637 "SELECT role FROM team_members WHERE team_id = ?1 AND user_id = ?2",
638 rusqlite::params![team_id, user.id],
639 |row| row.get(0),
640 )
641 .map_err(|_| {
642 (
643 StatusCode::FORBIDDEN,
644 "You are not a member of this team".to_string(),
645 )
646 })?;
647
648 if role != "owner" && role != "admin" {
649 return Err((
650 StatusCode::FORBIDDEN,
651 "Only owners and admins can create invites".to_string(),
652 ));
653 }
654
655 let now = crate::Relay::now();
656 let token = format!(
657 "inv_{}",
658 uuid::Uuid::new_v4().to_string().replace('-', "")
659 );
660
661 conn.execute(
662 "INSERT INTO invites (token, team_id, inviter_id, email, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
663 rusqlite::params![token, team_id, user.id, req.email, now],
664 )
665 .map_err(|e| {
666 (
667 StatusCode::INTERNAL_SERVER_ERROR,
668 format!("Failed to create invite: {}", e),
669 )
670 })?;
671
672 Ok((StatusCode::CREATED, Json(InviteResponse { token })))
673}
674
675async fn join_team(
676 State(state): State<SharedState>,
677 headers: HeaderMap,
678 Json(req): Json<JoinTeamRequest>,
679) -> Result<Json<hub::Team>, (StatusCode, String)> {
680 let user = extract_user(&state, &headers)?;
681 let conn = state.lock().unwrap();
682
683 let (team_id, invite_email): (String, Option<String>) = conn
685 .query_row(
686 "SELECT team_id, email FROM invites WHERE token = ?1 AND used_at IS NULL",
687 [&req.token],
688 |row| Ok((row.get(0)?, row.get(1)?)),
689 )
690 .map_err(|_| {
691 (
692 StatusCode::NOT_FOUND,
693 "Invalid or already used invite token".to_string(),
694 )
695 })?;
696
697 if let Some(ref email) = invite_email {
699 if email != &user.email {
700 return Err((
701 StatusCode::FORBIDDEN,
702 format!("This invite is for {}", email),
703 ));
704 }
705 }
706
707 let now = crate::Relay::now();
708
709 let already: bool = conn
711 .query_row(
712 "SELECT COUNT(*) FROM team_members WHERE team_id = ?1 AND user_id = ?2",
713 rusqlite::params![team_id, user.id],
714 |row| row.get::<_, i64>(0),
715 )
716 .map(|c| c > 0)
717 .unwrap_or(false);
718
719 if already {
720 return Err((
721 StatusCode::CONFLICT,
722 "You are already a member of this team".to_string(),
723 ));
724 }
725
726 conn.execute(
728 "INSERT INTO team_members (team_id, user_id, role, joined_at) VALUES (?1, ?2, ?3, ?4)",
729 rusqlite::params![team_id, user.id, "member", now],
730 )
731 .map_err(|e| {
732 (
733 StatusCode::INTERNAL_SERVER_ERROR,
734 format!("Failed to join team: {}", e),
735 )
736 })?;
737
738 let _ = conn.execute(
740 "UPDATE invites SET used_at = ?1 WHERE token = ?2",
741 rusqlite::params![now, req.token],
742 );
743
744 let team = conn
746 .query_row(
747 "SELECT id, name, owner_id, created_at FROM teams WHERE id = ?1",
748 [&team_id],
749 |row| {
750 Ok(hub::Team {
751 id: row.get(0)?,
752 name: row.get(1)?,
753 owner_id: row.get(2)?,
754 created_at: row.get(3)?,
755 })
756 },
757 )
758 .map_err(|e| {
759 (
760 StatusCode::INTERNAL_SERVER_ERROR,
761 format!("Failed to fetch team: {}", e),
762 )
763 })?;
764
765 Ok(Json(team))
766}
767
768async fn list_members(
769 State(state): State<SharedState>,
770 headers: HeaderMap,
771 AxumPath(team_id): AxumPath<String>,
772) -> Result<Json<Vec<hub::TeamMember>>, (StatusCode, String)> {
773 let user = extract_user(&state, &headers)?;
774 let conn = state.lock().unwrap();
775
776 conn.query_row(
778 "SELECT 1 FROM team_members WHERE team_id = ?1 AND user_id = ?2",
779 rusqlite::params![team_id, user.id],
780 |_| Ok(()),
781 )
782 .map_err(|_| {
783 (
784 StatusCode::FORBIDDEN,
785 "You are not a member of this team".to_string(),
786 )
787 })?;
788
789 let mut stmt = conn
790 .prepare(
791 "SELECT tm.user_id, tm.role, tm.joined_at, u.name, u.email
792 FROM team_members tm
793 JOIN users u ON u.id = tm.user_id
794 WHERE tm.team_id = ?1
795 ORDER BY tm.joined_at ASC",
796 )
797 .map_err(|e| {
798 (
799 StatusCode::INTERNAL_SERVER_ERROR,
800 format!("Query error: {}", e),
801 )
802 })?;
803
804 let members: Vec<hub::TeamMember> = stmt
805 .query_map([&team_id], |row| {
806 Ok(hub::TeamMember {
807 user_id: row.get(0)?,
808 role: row.get(1)?,
809 joined_at: row.get(2)?,
810 name: row.get(3)?,
811 email: row.get(4)?,
812 })
813 })
814 .map_err(|e| {
815 (
816 StatusCode::INTERNAL_SERVER_ERROR,
817 format!("Query error: {}", e),
818 )
819 })?
820 .filter_map(|r| r.ok())
821 .collect();
822
823 Ok(Json(members))
824}
825
826async fn kick_member(
827 State(state): State<SharedState>,
828 headers: HeaderMap,
829 AxumPath(team_id): AxumPath<String>,
830 Json(req): Json<KickRequest>,
831) -> Result<StatusCode, (StatusCode, String)> {
832 let user = extract_user(&state, &headers)?;
833 let conn = state.lock().unwrap();
834
835 let role: String = conn
837 .query_row(
838 "SELECT role FROM team_members WHERE team_id = ?1 AND user_id = ?2",
839 rusqlite::params![team_id, user.id],
840 |row| row.get(0),
841 )
842 .map_err(|_| {
843 (
844 StatusCode::FORBIDDEN,
845 "You are not a member of this team".to_string(),
846 )
847 })?;
848
849 if role != "owner" && role != "admin" {
850 return Err((
851 StatusCode::FORBIDDEN,
852 "Only owners and admins can kick members".to_string(),
853 ));
854 }
855
856 if req.user_id == user.id {
858 return Err((
859 StatusCode::BAD_REQUEST,
860 "Cannot kick yourself".to_string(),
861 ));
862 }
863
864 let target_role: String = conn
866 .query_row(
867 "SELECT role FROM team_members WHERE team_id = ?1 AND user_id = ?2",
868 rusqlite::params![team_id, req.user_id],
869 |row| row.get(0),
870 )
871 .map_err(|_| {
872 (
873 StatusCode::NOT_FOUND,
874 "User is not a member of this team".to_string(),
875 )
876 })?;
877
878 if target_role == "owner" {
879 return Err((
880 StatusCode::FORBIDDEN,
881 "Cannot kick the team owner".to_string(),
882 ));
883 }
884
885 conn.execute(
886 "DELETE FROM team_members WHERE team_id = ?1 AND user_id = ?2",
887 rusqlite::params![team_id, req.user_id],
888 )
889 .map_err(|e| {
890 (
891 StatusCode::INTERNAL_SERVER_ERROR,
892 format!("Failed to kick member: {}", e),
893 )
894 })?;
895
896 Ok(StatusCode::OK)
897}
898
899async fn create_channel(
902 State(state): State<SharedState>,
903 headers: HeaderMap,
904 AxumPath(team_id): AxumPath<String>,
905 Json(req): Json<CreateChannelRequest>,
906) -> Result<(StatusCode, Json<hub::Channel>), (StatusCode, String)> {
907 let user = extract_user(&state, &headers)?;
908 let conn = state.lock().unwrap();
909
910 conn.query_row(
912 "SELECT 1 FROM team_members WHERE team_id = ?1 AND user_id = ?2",
913 rusqlite::params![team_id, user.id],
914 |_| Ok(()),
915 )
916 .map_err(|_| {
917 (
918 StatusCode::FORBIDDEN,
919 "You are not a member of this team".to_string(),
920 )
921 })?;
922
923 let now = crate::Relay::now();
924 let channel_id = uuid::Uuid::new_v4().to_string();
925
926 conn.execute(
927 "INSERT INTO channels (id, team_id, name, created_at) VALUES (?1, ?2, ?3, ?4)",
928 rusqlite::params![channel_id, team_id, req.name, now],
929 )
930 .map_err(|e| {
931 if e.to_string().contains("UNIQUE") {
932 (
933 StatusCode::CONFLICT,
934 format!("Channel '{}' already exists in this team", req.name),
935 )
936 } else {
937 (
938 StatusCode::INTERNAL_SERVER_ERROR,
939 format!("Database error: {}", e),
940 )
941 }
942 })?;
943
944 let channel = hub::Channel {
945 id: channel_id,
946 name: req.name,
947 team_id,
948 };
949
950 Ok((StatusCode::CREATED, Json(channel)))
951}
952
953async fn list_channels(
954 State(state): State<SharedState>,
955 headers: HeaderMap,
956 AxumPath(team_id): AxumPath<String>,
957) -> Result<Json<Vec<hub::Channel>>, (StatusCode, String)> {
958 let user = extract_user(&state, &headers)?;
959 let conn = state.lock().unwrap();
960
961 conn.query_row(
963 "SELECT 1 FROM team_members WHERE team_id = ?1 AND user_id = ?2",
964 rusqlite::params![team_id, user.id],
965 |_| Ok(()),
966 )
967 .map_err(|_| {
968 (
969 StatusCode::FORBIDDEN,
970 "You are not a member of this team".to_string(),
971 )
972 })?;
973
974 let mut stmt = conn
975 .prepare(
976 "SELECT id, name, team_id FROM channels WHERE team_id = ?1 ORDER BY created_at ASC",
977 )
978 .map_err(|e| {
979 (
980 StatusCode::INTERNAL_SERVER_ERROR,
981 format!("Query error: {}", e),
982 )
983 })?;
984
985 let channels: Vec<hub::Channel> = stmt
986 .query_map([&team_id], |row| {
987 Ok(hub::Channel {
988 id: row.get(0)?,
989 name: row.get(1)?,
990 team_id: row.get(2)?,
991 })
992 })
993 .map_err(|e| {
994 (
995 StatusCode::INTERNAL_SERVER_ERROR,
996 format!("Query error: {}", e),
997 )
998 })?
999 .filter_map(|r| r.ok())
1000 .collect();
1001
1002 Ok(Json(channels))
1003}