🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2021-11-08 0:37:13.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-11-08 0:37:50.0 +00:00:00
commit
37f6aa90a473710e57ef65c4e8f03ad445b3c03e [patch]
tree
c82b4d6ae7e5e0ac89f8e520c5d8da0b6dd88b53
parent
7a00a389fa8b984c948b44e5771f36633693becc
download
37f6aa90a473710e57ef65c4e8f03ad445b3c03e.tar.gz

Implement automatic session extending

Fixes #30

Diff

 chartered-db/src/users.rs                          | 20 ++++++++++++++++++++
 chartered-frontend/src/useAuth.tsx                 | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 chartered-web/src/endpoints/web_api/auth/extend.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++
 chartered-web/src/endpoints/web_api/auth/mod.rs    |  5 ++++-
 4 files changed, 132 insertions(+), 13 deletions(-)

diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs
index 63530e7..a8a0710 100644
--- a/chartered-db/src/users.rs
+++ a/chartered-db/src/users.rs
@@ -403,6 +403,26 @@
        })
        .await?
    }

    pub async fn extend(
        self: Arc<Self>,
        conn: ConnectionPool,
        given_expires_at: chrono::NaiveDateTime,
    ) -> Result<bool> {
        use crate::schema::user_sessions::dsl::expires_at;

        tokio::task::spawn_blocking(move || {
            let conn = conn.get()?;

            let res = diesel::update(user_sessions::table)
                .filter(user_sessions::id.eq(self.id))
                .set(expires_at.eq(given_expires_at))
                .execute(&conn)?;

            Ok(res > 0)
        })
        .await?
    }
}

#[derive(Identifiable, Queryable, Associations, PartialEq, Eq, Hash, Debug)]
diff --git a/chartered-frontend/src/useAuth.tsx b/chartered-frontend/src/useAuth.tsx
index 85ae7eb..f576a74 100644
--- a/chartered-frontend/src/useAuth.tsx
+++ a/chartered-frontend/src/useAuth.tsx
@@ -19,6 +19,11 @@
  picture_url?: string;
}

interface ExtendResponse {
  expires: number;
  error?: string;
}

export interface AuthContext {
  login: (username: string, password: string) => Promise<void>;
  oauthLogin: (provider: string) => Promise<void>;
@@ -73,18 +78,6 @@
      authStorage.pictureUrl,
    ];
  });

  useEffect(() => {
    localStorage.setItem(
      "charteredAuthentication",
      JSON.stringify({
        userUuid: auth?.[0],
        authKey: auth?.[1],
        expires: auth?.[2],
        pictureUrl: auth?.[3],
      })
    );
  }, [auth]);

  const handleLoginResponse = (response: LoginResponse) => {
    if (response.error) {
@@ -158,6 +151,36 @@
      console.error("Failed to fully log user out of session", e);
    } finally {
      setAuth(null);
    }
  };

  const extendSession = async () => {
    if (auth === null || auth?.[2] < new Date()) {
      return;
    }

    try {
      const res = await fetch(
        `${BASE_URL}/a/${getAuthKey()}/web/v1/auth/extend`,
        {
          method: "GET",
          headers: {
            "Content-Type": "application/json",
            "User-Agent": window.navigator.userAgent,
          },
        }
      );
      const response: ExtendResponse = await res.json();

      if (response.error) {
        throw new Error(response.error);
      }

      const newAuth = [...auth];
      newAuth[2] = new Date(response.expires);
      setAuth(newAuth);
    } catch (e) {
      console.error("Failed to extend user session", e);
    }
  };

@@ -176,6 +199,30 @@
      return null;
    }
  };

  useEffect(() => {
    localStorage.setItem(
      "charteredAuthentication",
      JSON.stringify({
        userUuid: auth?.[0],
        authKey: auth?.[1],
        expires: auth?.[2],
        pictureUrl: auth?.[3],
      })
    );

    const extendInterval = 60000;
    if (auth?.[2] && auth?.[2].getTime() - new Date().getTime() <= 0) {
      extendSession();
    }

    const extendSessionIntervalId = setInterval(
      () => extendSession(),
      extendInterval
    );

    return () => clearInterval(extendSessionIntervalId);
  }, [auth]);

  return {
    login,
diff --git a/chartered-web/src/endpoints/web_api/auth/extend.rs b/chartered-web/src/endpoints/web_api/auth/extend.rs
new file mode 100644
index 0000000..2a58e50 100644
--- /dev/null
+++ a/chartered-web/src/endpoints/web_api/auth/extend.rs
@@ -1,0 +1,49 @@
//! Extends a user's session. Called on a loop from the web UI to keep

//! the user from logging out during periods of idleness from the API's

//! perspective.


use axum::{extract, Json};
use chartered_db::users::UserSession;
use chartered_db::ConnectionPool;
use serde::Serialize;
use std::sync::Arc;
use thiserror::Error;

pub async fn handle(
    extract::Extension(session): extract::Extension<Arc<UserSession>>,
    extract::Extension(db): extract::Extension<ConnectionPool>,
) -> Result<Json<ExtendResponse>, Error> {
    let expires = chrono::Utc::now() + chrono::Duration::hours(1);

    if session.extend(db, expires.naive_utc()).await? {
        Ok(Json(ExtendResponse { expires }))
    } else {
        Err(Error::InvalidSession)
    }
}

#[derive(Debug, Serialize)]
pub struct ExtendResponse {
    expires: chrono::DateTime<chrono::Utc>,
}

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

impl Error {
    pub fn status_code(&self) -> axum::http::StatusCode {
        use axum::http::StatusCode;

        match self {
            Self::Database(e) => e.status_code(),
            Self::InvalidSession => StatusCode::BAD_REQUEST,
        }
    }
}

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 e869b63..4f0f6cc 100644
--- a/chartered-web/src/endpoints/web_api/auth/mod.rs
+++ a/chartered-web/src/endpoints/web_api/auth/mod.rs
@@ -11,12 +11,15 @@

use serde::Serialize;

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

pub fn authenticated_routes() -> Router {
    Router::new().route("/logout", get(logout::handle))
    Router::new()
        .route("/logout", get(logout::handle))
        .route("/extend", get(extend::handle))
}

pub fn unauthenticated_routes() -> Router {