feat(user_panel): basic authorizations management
This commit is contained in:
parent
b20c30048c
commit
acb96dee39
12 changed files with 108 additions and 12 deletions
20
TODO.md
20
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
|
||||
|
|
0
src/controllers/ui/admin/apps.rs
Normal file
0
src/controllers/ui/admin/apps.rs
Normal file
0
src/controllers/ui/admin/authorizations.rs
Normal file
0
src/controllers/ui/admin/authorizations.rs
Normal file
0
src/controllers/ui/admin/users.rs
Normal file
0
src/controllers/ui/admin/users.rs
Normal file
|
@ -4,3 +4,4 @@ pub mod login;
|
|||
pub mod register;
|
||||
pub mod me;
|
||||
pub mod logout;
|
||||
pub mod user_panel;
|
||||
|
|
60
src/controllers/ui/user_panel/authorizations.rs
Normal file
60
src/controllers/ui/user_panel/authorizations.rs
Normal 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()
|
||||
}
|
1
src/controllers/ui/user_panel/mod.rs
Normal file
1
src/controllers/ui/user_panel/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod authorizations;
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 %}
|
||||
|
|
24
src/templates/pages/user_panel/authorizations.html
Normal file
24
src/templates/pages/user_panel/authorizations.html
Normal 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 %}
|
||||
|
|
@ -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()
|
||||
))
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue