🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2021-10-21 0:55:06.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-10-21 0:55:06.0 +01:00:00
commit
7cbd5c3f58318ae8c94f0d5b670a0611c0fe46a9 [patch]
tree
920882ef0080295d1c346eb3a0f02da2d0aec39e
parent
9622c20fe02a11857ebf9b32eb5d12be6e3e858f
download
7cbd5c3f58318ae8c94f0d5b670a0611c0fe46a9.tar.gz

Make the logout button destroy sessions in the database



Diff

 chartered-db/src/users.rs                          | 17 ++++++++++++++++-
 chartered-frontend/src/dark.sass                   |  2 +-
 chartered-frontend/src/useAuth.tsx                 | 36 ++++++++++++++++++++++++++++++++++--
 chartered-web/src/config.rs                        |  2 +-
 chartered-frontend/src/pages/Login.tsx             |  2 +-
 chartered-web/src/middleware/auth.rs               |  7 +++++--
 chartered-web/src/endpoints/web_api/mod.rs         |  3 ++-
 chartered-web/src/endpoints/web_api/auth/logout.rs | 38 ++++++++++++++++++++++++++++++++++++++
 chartered-web/src/endpoints/web_api/auth/mod.rs    | 24 ++++++++++++++++++++++--
 9 files changed, 109 insertions(+), 22 deletions(-)

diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs
index 75236c4..80391eb 100644
--- a/chartered-db/src/users.rs
+++ a/chartered-db/src/users.rs
@@ -85,7 +85,7 @@
    pub async fn find_by_session_key(
        conn: ConnectionPool,
        given_session_key: String,
    ) -> Result<Option<User>> {
    ) -> Result<Option<(UserSession, User)>> {
        use crate::schema::user_sessions::dsl::{expires_at, session_key};

        tokio::task::spawn_blocking(move || {
@@ -99,7 +99,7 @@
                )
                .filter(session_key.eq(given_session_key))
                .inner_join(users::table)
                .select(users::all_columns)
                .select((user_sessions::all_columns, users::all_columns))
                .get_result(&conn)
                .optional()?)
        })
@@ -334,6 +334,19 @@
            Ok(crate::schema::user_sessions::table
                .filter(session_key.eq(generated_session_key))
                .get_result(&conn)?)
        })
        .await?
    }

    pub async fn delete(self: Arc<Self>, conn: ConnectionPool) -> Result<bool> {
        tokio::task::spawn_blocking(move || {
            let conn = conn.get()?;

            let res = diesel::delete(user_sessions::table)
                .filter(user_sessions::id.eq(self.id))
                .execute(&conn)?;

            Ok(res > 0)
        })
        .await?
    }
diff --git a/chartered-frontend/src/dark.sass b/chartered-frontend/src/dark.sass
index 245dbd0..356f030 100644
--- a/chartered-frontend/src/dark.sass
+++ a/chartered-frontend/src/dark.sass
@@ -100,7 +100,7 @@
  code
    background: $card-border-color !important

  .form-control
  .form-control, .form-control:focus
    background: transparent !important
    color: white
    border-color: $card-border-color !important
diff --git a/chartered-frontend/src/useAuth.tsx b/chartered-frontend/src/useAuth.tsx
index 29fd9df..cf90e5e 100644
--- a/chartered-frontend/src/useAuth.tsx
+++ a/chartered-frontend/src/useAuth.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect, useContext, createContext } from "react";
import { useLocation, Redirect } from "react-router-dom";
import { unauthenticatedEndpoint } from "./util";
import {authenticatedEndpoint, BASE_URL, unauthenticatedEndpoint} from "./util";
import LoadingPage from "./pages/Loading";

export interface OAuthProviders {
@@ -40,7 +40,7 @@
  useEffect(async () => {
    try {
      let result = await fetch(
        unauthenticatedEndpoint(`login/oauth/complete${location.search}`)
        unauthenticatedEndpoint(`auth/login/oauth/complete${location.search}`)
      );
      let json = await result.json();

@@ -101,7 +101,7 @@
  };

  const login = async (username: string, password: string) => {
    let res = await fetch(unauthenticatedEndpoint("login/password"), {
    let res = await fetch(unauthenticatedEndpoint("auth/login/password"), {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
@@ -116,7 +116,7 @@

  const oauthLogin = async (provider: string) => {
    let res = await fetch(
      unauthenticatedEndpoint(`login/oauth/${provider}/begin`),
      unauthenticatedEndpoint(`auth/login/oauth/${provider}/begin`),
      {
        method: "GET",
        headers: {
@@ -134,16 +134,34 @@
    window.location.href = json.redirect_url;
  };

  const logout = async () => {
    // todo call the service so we can purge the key from the db
    setAuth(null);
  };

  const getAuthKey = () => {
    if (auth?.[2] > new Date()) {
      return auth?.[1];
    } else if (auth) {
      return null;
    }
  };

  const logout = async () => {
    if (auth === null) {
      return;
    }

    try {
      await fetch(
          `${BASE_URL}/a/${getAuthKey()}/web/v1/auth/logout`,
          {
            method: "GET",
            headers: {
              "Content-Type": "application/json",
              "User-Agent": window.navigator.userAgent,
            },
          }
      );
    } catch (e) {
      console.error("Failed to fully log user out of session", e)
    } finally {
      setAuth(null);
    }
  };

diff --git a/chartered-web/src/config.rs b/chartered-web/src/config.rs
index 20fbacb..e8e45a5 100644
--- a/chartered-web/src/config.rs
+++ a/chartered-web/src/config.rs
@@ -46,7 +46,7 @@
                        DiscoveredClient::discover(
                            config.client_id.to_string(),
                            config.client_secret.to_string(),
                            Some("http://127.0.0.1:1234/login/oauth".to_string()),
                            Some("http://127.0.0.1:1234/auth/login/oauth".to_string()),
                            config.discovery_uri.clone(),
                        )
                        .await?,
diff --git a/chartered-frontend/src/pages/Login.tsx b/chartered-frontend/src/pages/Login.tsx
index 299ef2a..e81a4bf 100644
--- a/chartered-frontend/src/pages/Login.tsx
+++ a/chartered-frontend/src/pages/Login.tsx
@@ -28,7 +28,7 @@

  const { response: oauthProviders } =
    useUnauthenticatedRequest<OAuthProviders>({
      endpoint: "login/oauth/providers",
      endpoint: "auth/login/oauth/providers",
    });

  useEffect(() => {
diff --git a/chartered-web/src/middleware/auth.rs b/chartered-web/src/middleware/auth.rs
index cde7c22..2544106 100644
--- a/chartered-web/src/middleware/auth.rs
+++ a/chartered-web/src/middleware/auth.rs
@@ -9,7 +9,9 @@
    collections::HashMap,
    task::{Context, Poll},
};
use std::sync::Arc;
use tower::Service;
use chartered_db::users::User;

use crate::endpoints::ErrorResponse;

@@ -52,11 +54,11 @@
                .unwrap()
                .clone();

            let user = match chartered_db::users::User::find_by_session_key(db, String::from(key))
            let (session, user) = match User::find_by_session_key(db, String::from(key))
                .await
                .unwrap()
            {
                Some(user) => std::sync::Arc::new(user),
                Some((session, user)) => (Arc::new(session), Arc::new(user)),
                None => {
                    return Ok(Response::builder()
                        .status(StatusCode::UNAUTHORIZED)
@@ -71,6 +73,7 @@
            };

            req.extensions_mut().unwrap().insert(user);
            req.extensions_mut().unwrap().insert(session);

            let response: Response<BoxBody> = inner.call(req.try_into_request().unwrap()).await?;

diff --git a/chartered-web/src/endpoints/web_api/mod.rs b/chartered-web/src/endpoints/web_api/mod.rs
index 407d039..08335b0 100644
--- a/chartered-web/src/endpoints/web_api/mod.rs
+++ a/chartered-web/src/endpoints/web_api/mod.rs
@@ -26,6 +26,7 @@
        .nest("/organisations", organisations::routes())
        .nest("/crates", crates::routes())
        .nest("/users", users::routes())
        .nest("/auth", auth::authenticated_routes())
        .route("/ssh-key", get(ssh_key::handle_get))
        .route("/ssh-key", put(ssh_key::handle_put))
        .route("/ssh-key/:id", delete(ssh_key::handle_delete)))
@@ -40,5 +41,5 @@
        > + Clone
        + Send,
> {
    crate::axum_box_after_every_route!(Router::new().nest("/login", auth::routes()))
    crate::axum_box_after_every_route!(Router::new().nest("/auth", auth::unauthenticated_routes()))
}
diff --git a/chartered-web/src/endpoints/web_api/auth/logout.rs b/chartered-web/src/endpoints/web_api/auth/logout.rs
new file mode 100644
index 0000000..3d0696e 100644
--- /dev/null
+++ a/chartered-web/src/endpoints/web_api/auth/logout.rs
@@ -1,0 +1,38 @@
use std::sync::Arc;
use axum::{extract, Json};
use chartered_db::ConnectionPool;
use serde::Serialize;
use thiserror::Error;
use chartered_db::users::UserSession;

pub async fn handle(
    extract::Extension(session): extract::Extension<Arc<UserSession>>,
    extract::Extension(db): extract::Extension<ConnectionPool>,
) -> Result<Json<LogoutResponse>, Error> {
    session.delete(db).await?;

    Ok(Json(LogoutResponse {
        success: true,
    }))
}

#[derive(Debug, Serialize)]
pub struct LogoutResponse {
    success: bool,
}

#[derive(Error, Debug)]
pub enum Error {
    #[error("{0}")]
    Database(#[from] chartered_db::Error),
}

impl Error {
    pub fn status_code(&self) -> axum::http::StatusCode {
        match self {
            Self::Database(e) => e.status_code(),
        }
    }
}

define_error_response!(Error);
diff --git a/chartered-web/src/endpoints/web_api/auth/mod.rs b/chartered-web/src/endpoints/web_api/auth/mod.rs
index 1b3303e..1445227 100644
--- a/chartered-web/src/endpoints/web_api/auth/mod.rs
+++ a/chartered-web/src/endpoints/web_api/auth/mod.rs
@@ -16,9 +16,23 @@

pub mod openid;
pub mod password;
pub mod logout;

pub fn routes() -> Router<
pub fn authenticated_routes() -> Router<
    impl tower::Service<
        Request<Body>,
        Response = Response<BoxBody>,
        Error = Infallible,
        Future = impl Future<Output = Result<Response<BoxBody>, Infallible>> + Send,
    > + Clone
    + Send,
> {
    crate::axum_box_after_every_route!(Router::new()
        .route("/logout", get(logout::handle)))
}

pub fn unauthenticated_routes() -> Router<
    impl tower::Service<
            Request<Body>,
            Response = Response<BoxBody>,
            Error = Infallible,
@@ -27,10 +41,10 @@
        + Send,
> {
    crate::axum_box_after_every_route!(Router::new()
        .route("/password", post(password::handle))
        .route("/oauth/:provider/begin", get(openid::begin_oidc))
        .route("/oauth/complete", get(openid::complete_oidc))
        .route("/oauth/providers", get(openid::list_providers)))
        .route("/login/password", post(password::handle))
        .route("/login/oauth/:provider/begin", get(openid::begin_oidc))
        .route("/login/oauth/complete", get(openid::complete_oidc))
        .route("/login/oauth/providers", get(openid::list_providers)))
}

#[derive(Serialize)]