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