Compare commits

..

1 commit

Author SHA1 Message Date
084fea134b WIP 2024-11-18 08:58:38 +01:00
10 changed files with 113 additions and 10 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 - [ ] Support error responses by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
- [ ] UserWebGUI: Redirect to login when JWT expire - [x] UserWebGUI: Redirect to login when JWT expire
- [ ] UserWebGUI: Show user authorizations. - [x] UserWebGUI: Show user authorizations.
- [ ] UserWebGUI: Allow to revoke an authorization
- [ ] UserWebGUI: Show available apps - [ ] UserWebGUI: Show available apps
- [ ] UserWebGUI: Direct user grant flow, User can login to the target app/client, event if it did - [ ] UserWebGUI: Direct user grant flow, User can login to the target app/client, event if it did not started here.
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 - [ ] Add admin panel via API
- [ ] AdminWebGUI: Ability to create invitation links - [ ] AdminWebGUI: Ability to create invitation links
- [ ] Add admin CLI - [ ] Add admin CLI
- [ ] add TOTP

View file

View file

View file

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

View file

@ -0,0 +1,70 @@
use axum::{extract::State, response::IntoResponse, Extension, Form};
use minijinja::context;
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 {
client_id: String
}
pub async fn revoke_authorization(
State(app_state): State<AppState>,
Extension(renderer): Extension<TemplateRenderer>,
Extension(token_claims): Extension<UserTokenClaims>,
Form(form): Form<RevokeAuthorizationForm>
) -> impl IntoResponse {
let update_res = sqlx::query("DELETE FROM authorizations WHERE id = $1")
.bind(&form.client_id)
.execute(&app_state.db)
.await;
let user_res = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(&token_claims.sub)
.fetch_one(&app_state.db)
.await
.expect("To get user from claim");
match update_res {
Ok(_) => {
renderer.render(
template_path,
context!(
success => true,
user => user_res
)
)
},
Err(err) => {
error!("Cannot update user details. {}", err);
renderer.render(
template_path,
context!(
error => Some("Cannot update user details.".to_string()),
user => user_res
)
)
}
}
}

View file

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

View file

@ -12,7 +12,6 @@ pub async fn renderer_middleware(
env: app_state.templating_env, env: app_state.templating_env,
token_claims: token_claims_ext.map(|x| x.0) token_claims: token_claims_ext.map(|x| x.0)
}; };
dbg!(&renderer_instance);
req.extensions_mut().insert(renderer_instance); req.extensions_mut().insert(renderer_instance);
Ok(next.run(req).await) Ok(next.run(req).await)
} }

View file

@ -23,12 +23,15 @@ pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router
.layer(middleware::from_fn_with_state(app_state.clone(), user_auth::auth_middleware)); .layer(middleware::from_fn_with_state(app_state.clone(), user_auth::auth_middleware));
let user_routes = Router::new() 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("/logout", get(ui::logout::perform_logout))
.route("/authorize", get(ui::authorize::authorize_form)) .route("/authorize", get(ui::authorize::authorize_form))
.route("/authorize", post(ui::authorize::perform_authorize)) .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))
.route("/me/authorizations/reset-all", post(ui::me::me_perform_update_details))
.layer(middleware::from_fn_with_state(app_state.clone(), renderer_middleware)) .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::enforce_auth_middleware))
.layer(middleware::from_fn_with_state(app_state.clone(), user_auth::auth_middleware)); .layer(middleware::from_fn_with_state(app_state.clone(), user_auth::auth_middleware));

View file

@ -0,0 +1,21 @@
{% extends "layouts/base.html" %}
{% block body %}
<h1>Your authorizations</h1>
<p>
<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="client_id" value="{{ item.client_id }}" />
<button class="btn btn-primary">Revoke</button>
</form>
</li>
{% endfor %}
</ul>
</p>
{% endblock %}