diff --git a/TODO.md b/TODO.md index ec8bc9b..26607cf 100644 --- a/TODO.md +++ b/TODO.md @@ -28,13 +28,21 @@ - [ ] Support error responses by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 -- [ ] UserWebGUI: Redirect to login when JWT expire -- [ ] UserWebGUI: Show user authorizations. +- [x] UserWebGUI: Redirect to login when JWT expire +- [x] UserWebGUI: Show user authorizations. +- [ ] UserWebGUI: Allow to revoke an authorization - [ ] UserWebGUI: Show available apps -- [ ] UserWebGUI: Direct user grant flow, User can login to the target app/client, event if it did -not started here. +- [ ] UserWebGUI: Direct user grant flow, User can login to the target app/client, event if it did not started here. + - all apps must have a `/oauth2/login` URL that redirect to the right minauth /authorize URL + +- [ ] UserWebGUI: add TOTP +- [ ] send emails to users + +- Architecture: do we have an admin API? + +- [ ] AdminCLI: init +- [ ] AdminCLI: create invitation links + - [ ] Add admin panel via API - [ ] AdminWebGUI: Ability to create invitation links - [ ] Add admin CLI - -- [ ] add TOTP diff --git a/src/controllers/ui/admin/apps.rs b/src/controllers/ui/admin/apps.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/ui/admin/authorizations.rs b/src/controllers/ui/admin/authorizations.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/ui/admin/users.rs b/src/controllers/ui/admin/users.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/controllers/ui/mod.rs b/src/controllers/ui/mod.rs index 5910f89..37188bb 100644 --- a/src/controllers/ui/mod.rs +++ b/src/controllers/ui/mod.rs @@ -4,3 +4,4 @@ pub mod login; pub mod register; pub mod me; pub mod logout; +pub mod user_panel; diff --git a/src/controllers/ui/user_panel/authorizations.rs b/src/controllers/ui/user_panel/authorizations.rs new file mode 100644 index 0000000..551b769 --- /dev/null +++ b/src/controllers/ui/user_panel/authorizations.rs @@ -0,0 +1,60 @@ +use axum::{extract::State, http::StatusCode, response::{Html, IntoResponse, Redirect}, Extension, Form}; +use fully_pub::fully_pub; +use log::error; +use minijinja::context; +use serde::Deserialize; + +use crate::{models::{authorization::Authorization, token_claims::UserTokenClaims}, renderer::TemplateRenderer, server::AppState}; + +pub async fn get_authorizations( + State(app_state): State, + Extension(renderer): Extension, + Extension(token_claims): Extension, +) -> impl IntoResponse { + let user_authorizations = sqlx::query_as::<_, Authorization>("SELECT * FROM authorizations WHERE user_id = $1") + .bind(&token_claims.sub) + .fetch_all(&app_state.db) + .await + .expect("To get user authorization with user_id from claim"); + renderer.render( + "pages/user_panel/authorizations", + context!( + user_authorizations => user_authorizations + ) + ) +} + + +#[derive(Debug, Deserialize)] +#[fully_pub] +struct RevokeAuthorizationForm { + authorization_id: String +} + +pub async fn revoke_authorization( + State(app_state): State, + Form(form): Form +) -> impl IntoResponse { + let delete_res = sqlx::query("DELETE FROM authorizations WHERE id = $1") + .bind(&form.authorization_id) + .execute(&app_state.db) + .await; + match delete_res { + Ok(_) => {}, + Err(sqlx::Error::RowNotFound) => { + return ( + StatusCode::BAD_REQUEST, + Html("Could not find authorization.") + ).into_response(); + }, + Err(err) => { + error!("Failed to delete authorization, {}", err); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Html("Failed to delete authorization.") + ).into_response(); + } + } + + Redirect::to("/me/authorizations").into_response() +} diff --git a/src/controllers/ui/user_panel/mod.rs b/src/controllers/ui/user_panel/mod.rs new file mode 100644 index 0000000..11834ae --- /dev/null +++ b/src/controllers/ui/user_panel/mod.rs @@ -0,0 +1 @@ +pub mod authorizations; diff --git a/src/middlewares/renderer.rs b/src/middlewares/renderer.rs index 21f00f7..0e01238 100644 --- a/src/middlewares/renderer.rs +++ b/src/middlewares/renderer.rs @@ -12,7 +12,6 @@ pub async fn renderer_middleware( env: app_state.templating_env, token_claims: token_claims_ext.map(|x| x.0) }; - dbg!(&renderer_instance); req.extensions_mut().insert(renderer_instance); Ok(next.run(req).await) } diff --git a/src/router.rs b/src/router.rs index 9082f6d..27e3ff7 100644 --- a/src/router.rs +++ b/src/router.rs @@ -23,12 +23,14 @@ pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router .layer(middleware::from_fn_with_state(app_state.clone(), user_auth::auth_middleware)); let user_routes = Router::new() - .route("/me", get(ui::me::me_page)) - .route("/me/details-form", get(ui::me::me_update_details_form)) - .route("/me/details-form", post(ui::me::me_perform_update_details)) .route("/logout", get(ui::logout::perform_logout)) .route("/authorize", get(ui::authorize::authorize_form)) .route("/authorize", post(ui::authorize::perform_authorize)) + .route("/me", get(ui::me::me_page)) + .route("/me/details-form", get(ui::me::me_update_details_form)) + .route("/me/details-form", post(ui::me::me_perform_update_details)) + .route("/me/authorizations", get(ui::user_panel::authorizations::get_authorizations)) + .route("/me/authorizations/revoke", post(ui::user_panel::authorizations::revoke_authorization)) .layer(middleware::from_fn_with_state(app_state.clone(), renderer_middleware)) .layer(middleware::from_fn_with_state(app_state.clone(), user_auth::enforce_auth_middleware)) .layer(middleware::from_fn_with_state(app_state.clone(), user_auth::auth_middleware)); diff --git a/src/templates/pages/me/index.html b/src/templates/pages/me/index.html index 3ab2ee7..a886ad4 100644 --- a/src/templates/pages/me/index.html +++ b/src/templates/pages/me/index.html @@ -2,7 +2,8 @@ {% block body %}

Welcome {{ user.full_name or user.handle }}!

-Update details +Update details. +Manage authorizations.

{% if user.picture %} diff --git a/src/templates/pages/user_panel/authorizations.html b/src/templates/pages/user_panel/authorizations.html new file mode 100644 index 0000000..e49d288 --- /dev/null +++ b/src/templates/pages/user_panel/authorizations.html @@ -0,0 +1,24 @@ +{% extends "layouts/base.html" %} +{% block body %} +

Your authorizations

+ +

+ {% if user_authorizations | length == 0 %} + You didn't authorized or accessed any applications for now. + {% endif %} +

    + {% for item in user_authorizations %} +
  • + {{ item.client_id }} + Scopes: {{ item.scopes }} + Last_used_at: {{ item.last_used_at }} +
    + + +
    +
  • + {% endfor %} +
+

+{% endblock %} + diff --git a/src/utils.rs b/src/utils.rs index 7486772..4afce48 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -59,5 +59,5 @@ pub fn parse_basic_auth(header_value: &str) -> Result<(String, String)> { components.first().ok_or(anyhow!("Expected username in encoded Authorization header value."))?.to_string(), components.get(1).ok_or(anyhow!("Expected password in encoded Authorization header value."))?.to_string() )) - } +