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(-)
@@ -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?
}
@@ -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
@@ -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 () => {
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);
}
};
@@ -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?,
@@ -28,7 +28,7 @@
const { response: oauthProviders } =
useUnauthenticatedRequest<OAuthProviders>({
endpoint: "login/oauth/providers",
endpoint: "auth/login/oauth/providers",
});
useEffect(() => {
@@ -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?;
@@ -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()))
}
@@ -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);
@@ -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)]