feat(user_panel): basic authorizations management

This commit is contained in:
Matthieu Bessat 2024-11-18 08:58:38 +01:00
parent b20c30048c
commit acb96dee39
12 changed files with 108 additions and 12 deletions

20
TODO.md
View file

@ -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

View file

View file

View file

@ -4,3 +4,4 @@ pub mod login;
pub mod register;
pub mod me;
pub mod logout;
pub mod user_panel;

View file

@ -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<AppState>,
Extension(renderer): Extension<TemplateRenderer>,
Extension(token_claims): Extension<UserTokenClaims>,
) -> 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<AppState>,
Form(form): Form<RevokeAuthorizationForm>
) -> 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()
}

View file

@ -0,0 +1 @@
pub mod authorizations;

View file

@ -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)
}

View file

@ -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));

View file

@ -2,7 +2,8 @@
{% block body %}
<h1>Welcome {{ user.full_name or user.handle }}!</h1>
<a href="/me/details-form">Update details</a>
<a href="/me/details-form">Update details.</a>
<a href="/me/authorizations">Manage authorizations.</a>
<p>
{% if user.picture %}

View file

@ -0,0 +1,24 @@
{% extends "layouts/base.html" %}
{% block body %}
<h1>Your authorizations</h1>
<p>
{% if user_authorizations | length == 0 %}
<i>You didn't authorized or accessed any applications for now.</i>
{% endif %}
<ul>
{% for item in user_authorizations %}
<li>
{{ item.client_id }}
Scopes: {{ item.scopes }}
Last_used_at: {{ item.last_used_at }}
<form method="post" action="/me/authorizations/revoke">
<input type="hidden" name="authorization_id" value="{{ item.id }}" />
<button class="btn btn-primary">Revoke</button>
</form>
</li>
{% endfor %}
</ul>
</p>
{% endblock %}

View file

@ -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()
))
}