Compare commits

...

1 commit

Author SHA1 Message Date
330add1f17 WIP 2024-11-22 17:57:22 +01:00
11 changed files with 93 additions and 11 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,49 @@
use axum::{extract::State, response::{IntoResponse, Redirect}, Extension, Form};
use axum_extra::extract::{cookie::Cookie, CookieJar};
use fully_pub::fully_pub;
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>,
Extension(renderer): Extension<TemplateRenderer>,
Extension(token_claims): Extension<UserTokenClaims>,
cookies: CookieJar,
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;
(
Redirect::temporary("/me/authorizations")
)
}

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 %}

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.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() components.get(1).ok_or(anyhow!("Expected password in encoded Authorization header value."))?.to_string()
)) ))
} }