linkrs_server/http/handlers/
session.rs1use axum::{
2 extract::{Extension, Json, Path, State},
3 http::StatusCode,
4 response::Json as JsonResponse,
5};
6use linkrs_wire::meta::{CreateSessionRequest, SessionResponse};
7
8use crate::http::{error::HttpError, state::AppState};
9use crate::storage::{
10 StorageClient, StorageOperationContextOps, StorageSchemaContextOps, StorageSyncContextOps,
11};
12
13#[utoipa::path(
14 post,
15 operation_id = "post_v1_sessions",
16 path = "/v1/sessions",
17 tag = "Session",
18 request_body = CreateSessionRequest,
19 responses(
20 (status = 200, body = SessionResponse, description = "Session created"),
21 (status = 401, description = "Invalid credentials"),
22 (status = 403, description = "Account locked"),
23 (status = 500, description = "Internal error")
24 )
25)]
26pub async fn create<
27 S: StorageClient
28 + StorageSchemaContextOps
29 + StorageSyncContextOps
30 + StorageOperationContextOps
31 + Clone
32 + Send
33 + Sync
34 + 'static,
35>(
36 State(state): State<AppState<S>>,
37 Json(request): Json<CreateSessionRequest>,
38) -> Result<JsonResponse<SessionResponse>, HttpError> {
39 let graph_service = state.server.get_graph_service();
40
41 if graph_service.is_user_locked(&request.username) {
43 return Err(HttpError::forbidden(format!(
44 "account {} is locked",
45 request.username
46 )));
47 }
48
49 if !graph_service.is_auth_disabled() {
54 let password = request
55 .password
56 .as_deref()
57 .filter(|p| !p.is_empty())
58 .ok_or_else(|| HttpError::unauthorized("password required"))?;
59
60 let session = graph_service
61 .authenticate(&request.username, password)
62 .await
63 .map_err(|message| {
64 let lowered = message.to_lowercase();
65 if lowered.contains("locked") {
66 HttpError::forbidden(message)
67 } else {
68 HttpError::unauthorized(message)
69 }
70 })?;
71 return Ok(JsonResponse(SessionResponse {
72 session_id: session.id(),
73 username: session.user(),
74 created_at: std::time::SystemTime::now()
75 .duration_since(std::time::UNIX_EPOCH)
76 .expect("SystemTime before UNIX_EPOCH")
77 .as_secs(),
78 }));
79 }
80
81 let session_manager = state.server.get_session_manager();
82 let session = session_manager
83 .create_session(request.username.clone(), request.client_ip)
84 .await
85 .map_err(|e| HttpError::BadRequest(format!("Failed to create session: {}", e)))?;
86
87 Ok(JsonResponse(SessionResponse {
88 session_id: session.id(),
89 username: session.user(),
90 created_at: std::time::SystemTime::now()
91 .duration_since(std::time::UNIX_EPOCH)
92 .expect("SystemTime before UNIX_EPOCH")
93 .as_secs(),
94 }))
95}
96
97#[utoipa::path(
98 get,
99 operation_id = "get_v1_sessions",
100 path = "/v1/sessions",
101 tag = "Session",
102 responses(
103 (status = 200, body = serde_json::Value, description = "Session list"),
104 (status = 403, description = "Forbidden"),
105 (status = 500, description = "Internal error")
106 )
107)]
108pub async fn list_sessions<
109 S: StorageClient
110 + StorageSchemaContextOps
111 + StorageSyncContextOps
112 + StorageOperationContextOps
113 + Clone
114 + Send
115 + Sync
116 + 'static,
117>(
118 State(state): State<AppState<S>>,
119 Extension(session_id): Extension<i64>,
120) -> Result<JsonResponse<serde_json::Value>, HttpError> {
121 super::authz::require_admin_session(&state, session_id)?;
122 let sessions = state.server.get_graph_service().list_sessions().await;
123 let items: Vec<serde_json::Value> = sessions
124 .into_iter()
125 .map(|info| {
126 serde_json::json!({
127 "session_id": info.session_id,
128 "username": info.user_name,
129 "space_name": info.space_name,
130 "graph_addr": info.graph_addr,
131 "active_queries": info.active_queries,
132 })
133 })
134 .collect();
135 Ok(JsonResponse(serde_json::json!({ "sessions": items })))
136}
137
138#[utoipa::path(
139 get,
140 path = "/v1/sessions/{id}",
141 tag = "Session",
142 params(("id" = i64, Path, description = "Session id")),
143 responses(
144 (status = 200, body = serde_json::Value, description = "Session details"),
145 (status = 404, description = "Not found"),
146 (status = 500, description = "Not found")
147 )
148)]
149pub async fn get_session<
150 S: StorageClient
151 + StorageSchemaContextOps
152 + StorageSyncContextOps
153 + StorageOperationContextOps
154 + Clone
155 + Send
156 + Sync
157 + 'static,
158>(
159 State(state): State<AppState<S>>,
160 Extension(caller_session_id): Extension<i64>,
161 Path(session_id): Path<i64>,
162) -> Result<JsonResponse<serde_json::Value>, HttpError> {
163 let caller = super::authz::find_session(&state, caller_session_id)?;
164 let graph_service = state.server.get_graph_service();
165 let session_manager = state.server.get_session_manager();
166 let session = session_manager
167 .find_session(session_id)
168 .ok_or_else(|| HttpError::NotFound("Session not found".to_string()))?;
169 let is_admin = graph_service
170 .get_permission_manager()
171 .is_admin(&caller.user());
172 if !is_admin && session.user() != caller.user() {
173 return Err(HttpError::forbidden("Admin permission required"));
174 }
175
176 Ok(JsonResponse(serde_json::json!({
177 "session_id": session.id(),
178 "username": session.user(),
179 "space_name": session.space_name(),
180 "graph_addr": session.graph_addr(),
181 "timezone": session.timezone(),
182 })))
183}
184
185#[utoipa::path(
186 delete,
187 path = "/v1/sessions/{id}",
188 tag = "Session",
189 params(("id" = i64, Path, description = "Session id")),
190 responses(
191 (status = 204, description = "Session deleted"),
192 (status = 500, description = "Internal error")
193 )
194)]
195pub async fn delete_session<
196 S: StorageClient
197 + StorageSchemaContextOps
198 + StorageSyncContextOps
199 + StorageOperationContextOps
200 + Clone
201 + Send
202 + Sync
203 + 'static,
204>(
205 State(state): State<AppState<S>>,
206 Extension(caller_session_id): Extension<i64>,
207 Path(session_id): Path<i64>,
208) -> Result<StatusCode, HttpError> {
209 let caller = super::authz::find_session(&state, caller_session_id)?;
210 state
211 .server
212 .get_graph_service()
213 .kill_session(session_id, &caller.user())
214 .await
215 .map_err(|e| {
216 let msg = e.to_string().to_lowercase();
217 if msg.contains("not found") {
218 HttpError::NotFound(e.to_string())
219 } else if msg.contains("permission") {
220 HttpError::forbidden(e.to_string())
221 } else {
222 HttpError::BadRequest(e.to_string())
223 }
224 })?;
225 Ok(StatusCode::NO_CONTENT)
226}