From 0437bc9760b616fd198678f5b53932eba09f631f Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 08 Nov 2021 01:18:56 +0000 Subject: [PATCH] Implement session deleting via the session UI Fixes #17 --- chartered-db/src/schema.rs | 1 + chartered-db/src/users.rs | 17 +++++++++++++++++ migrations/2021-11-08-005634_update_sessions_add_uuid/down.sql | 2 ++ migrations/2021-11-08-005634_update_sessions_add_uuid/up.sql | 5 +++++ chartered-frontend/src/pages/sessions/ListSessions.tsx | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- chartered-web/src/endpoints/web_api/sessions/delete.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-web/src/endpoints/web_api/sessions/list.rs | 2 ++ chartered-web/src/endpoints/web_api/sessions/mod.rs | 3 ++- 8 files changed, 193 insertions(+), 10 deletions(-) diff --git a/chartered-db/src/schema.rs b/chartered-db/src/schema.rs index b519ab2..9f6da57 100644 --- a/chartered-db/src/schema.rs +++ a/chartered-db/src/schema.rs @@ -75,6 +75,7 @@ expires_at -> Nullable, user_agent -> Nullable, ip -> Nullable, + uuid -> Binary, } } diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index a8a0710..1798c12 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -327,6 +327,7 @@ pub expires_at: Option, pub user_agent: Option, pub ip: Option, + pub uuid: SqlUuid, } impl UserSession { @@ -339,7 +340,7 @@ given_ip: Option, ) -> Result { use crate::schema::user_sessions::dsl::{ - expires_at, ip, session_key, user_agent, user_id, user_sessions, user_ssh_key_id, + expires_at, ip, session_key, user_agent, user_id, user_sessions, user_ssh_key_id, uuid, }; tokio::task::spawn_blocking(move || { @@ -359,6 +360,7 @@ expires_at.eq(given_expires_at), user_agent.eq(given_user_agent), ip.eq(given_ip), + uuid.eq(SqlUuid::random()), )) .execute(&conn)?; @@ -397,6 +399,19 @@ let res = diesel::delete(user_sessions::table) .filter(user_sessions::id.eq(self.id)) + .execute(&conn)?; + + Ok(res > 0) + }) + .await? + } + + pub async fn delete_by_uuid(conn: ConnectionPool, uuid: uuid::Uuid) -> Result { + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + let res = diesel::delete(user_sessions::table) + .filter(user_sessions::uuid.eq(SqlUuid(uuid))) .execute(&conn)?; Ok(res > 0) diff --git a/migrations/2021-11-08-005634_update_sessions_add_uuid/down.sql b/migrations/2021-11-08-005634_update_sessions_add_uuid/down.sql new file mode 100644 index 0000000..99c116b 100644 --- /dev/null +++ a/migrations/2021-11-08-005634_update_sessions_add_uuid/down.sql @@ -1,0 +1,2 @@ +DROP INDEX unique_user_sessions_uuid; +ALTER TABLE user_sessions DROP COLUMN uuid; diff --git a/migrations/2021-11-08-005634_update_sessions_add_uuid/up.sql b/migrations/2021-11-08-005634_update_sessions_add_uuid/up.sql new file mode 100644 index 0000000..dc7d397 100644 --- /dev/null +++ a/migrations/2021-11-08-005634_update_sessions_add_uuid/up.sql @@ -1,0 +1,5 @@ +-- drop all sessions before creating NOT NULL column +DELETE FROM user_sessions; + +ALTER TABLE user_sessions ADD COLUMN uuid BINARY(128) NOT NULL; +CREATE UNIQUE INDEX unique_user_sessions_uuid ON user_sessions(uuid); diff --git a/chartered-frontend/src/pages/sessions/ListSessions.tsx b/chartered-frontend/src/pages/sessions/ListSessions.tsx index 2124747..c8b4908 100644 --- a/chartered-frontend/src/pages/sessions/ListSessions.tsx +++ a/chartered-frontend/src/pages/sessions/ListSessions.tsx @@ -1,17 +1,24 @@ import Nav from "../../sections/Nav"; import { useAuth } from "../../useAuth"; import { Link, Navigate } from "react-router-dom"; -import { RoundedPicture, useAuthenticatedRequest } from "../../util"; +import { + authenticatedEndpoint, + RoundedPicture, + useAuthenticatedRequest, +} from "../../util"; import ErrorPage from "../ErrorPage"; import { LoadingSpinner } from "../Loading"; -import { OverlayTrigger, Tooltip } from "react-bootstrap"; +import { Button, Modal, OverlayTrigger, Tooltip } from "react-bootstrap"; import HumanTime from "react-human-time"; +import { XOctagonFill } from "react-bootstrap-icons"; +import { useState } from "react"; interface Response { sessions: ResponseSession[]; } interface ResponseSession { + uuid: string; expires_at: string | null; user_agent: string | null; ip: string | null; @@ -20,27 +27,78 @@ export default function ListSessions() { const auth = useAuth(); + const [deleting, setDeleting] = useState(null); + const [error, setError] = useState(""); + const [reloadSessions, setReloadSessions] = useState(0); if (!auth) { return ; } - const { response: list, error } = useAuthenticatedRequest({ - auth, - endpoint: "sessions", - }); + const { response: list, error: loadError } = + useAuthenticatedRequest( + { + auth, + endpoint: "sessions", + }, + [reloadSessions] + ); - if (error) { - return ; + if (loadError) { + return ; } + const deleteSession = async () => { + setError(""); + + if (!deleting) { + return; + } + + try { + let res = await fetch(authenticatedEndpoint(auth, "sessions"), { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ uuid: deleting }), + }); + let json = await res.json(); + + if (json.error) { + throw new Error(json.error); + } + + setReloadSessions(reloadSessions + 1); + } catch (e: any) { + setError(e.message); + } finally { + setDeleting(null); + } + }; + return (
+ ); +} + +function DeleteModal(props: { + show: boolean; + onCancel: () => void; + onConfirm: () => void; +}) { + return ( + + + + Are you sure you wish to delete this session? + + + + + + + ); } diff --git a/chartered-web/src/endpoints/web_api/sessions/delete.rs b/chartered-web/src/endpoints/web_api/sessions/delete.rs new file mode 100644 index 0000000..7f38782 100644 --- /dev/null +++ a/chartered-web/src/endpoints/web_api/sessions/delete.rs @@ -1,0 +1,53 @@ +use axum::{extract, Json}; +use chartered_db::{users::UserSession, ConnectionPool}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +pub async fn handle_delete( + extract::Extension(db): extract::Extension, + extract::Json(req): extract::Json, +) -> Result, Error> { + if UserSession::delete_by_uuid(db.clone(), req.uuid).await? { + Ok(Json(Response { success: true })) + } else { + Err(Error::UnknownSession) + } +} + +#[derive(Deserialize)] +pub struct Request { + uuid: chartered_db::uuid::Uuid, +} + +#[derive(Serialize)] +pub struct Response { + success: bool, +} + +#[derive(Serialize)] +pub struct ResponseSession { + uuid: chartered_db::uuid::Uuid, + expires_at: Option, + user_agent: Option, + ip: Option, + ssh_key_fingerprint: Option, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), + #[error("Unknown session")] + UnknownSession, +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + match self { + Self::Database(e) => e.status_code(), + Self::UnknownSession => axum::http::StatusCode::BAD_REQUEST, + } + } +} + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/sessions/list.rs b/chartered-web/src/endpoints/web_api/sessions/list.rs index ec482da..4d01136 100644 --- a/chartered-web/src/endpoints/web_api/sessions/list.rs +++ a/chartered-web/src/endpoints/web_api/sessions/list.rs @@ -15,6 +15,7 @@ sessions: sessions .into_iter() .map(|(session, ssh_key)| ResponseSession { + uuid: session.uuid.0, expires_at: session.expires_at, user_agent: session.user_agent, ip: session.ip, @@ -32,6 +33,7 @@ #[derive(Serialize)] pub struct ResponseSession { + uuid: chartered_db::uuid::Uuid, expires_at: Option, user_agent: Option, ip: Option, diff --git a/chartered-web/src/endpoints/web_api/sessions/mod.rs b/chartered-web/src/endpoints/web_api/sessions/mod.rs index 35ebad4..b234f9c 100644 --- a/chartered-web/src/endpoints/web_api/sessions/mod.rs +++ a/chartered-web/src/endpoints/web_api/sessions/mod.rs @@ -1,7 +1,8 @@ +mod delete; mod list; use axum::{routing::get, Router}; pub fn routes() -> Router { - Router::new().route("/", get(list::handle_get)) + Router::new().route("/", get(list::handle_get).delete(delete::handle_delete)) } -- rgit 0.1.3