Compare commits
	
		
			6 Commits
		
	
	
		
			ad58d2de7e
			...
			9a4c725b4e
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9a4c725b4e | |||
| f08fddc79c | |||
| da74acaed8 | |||
| 91fd763fe1 | |||
| 9e72e6a044 | |||
| cb4daa1112 | 
							
								
								
									
										7
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -395,6 +395,7 @@ dependencies = [ | |||||||
|  "mime_guess", |  "mime_guess", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  |  "urlencoding", | ||||||
|  "uuid", |  "uuid", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| @@ -1625,6 +1626,12 @@ dependencies = [ | |||||||
|  "percent-encoding", |  "percent-encoding", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "urlencoding" | ||||||
|  | version = "2.1.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "uuid" | name = "uuid" | ||||||
| version = "0.8.2" | version = "0.8.2" | ||||||
|   | |||||||
| @@ -20,3 +20,4 @@ uuid = { version = "0.8.2", features = ["v4"] } | |||||||
| mime_guess = "2.0.4" | mime_guess = "2.0.4" | ||||||
| askama = "0.11.1" | askama = "0.11.1" | ||||||
| futures-util = "0.3.21" | futures-util = "0.3.21" | ||||||
|  | urlencoding = "2.1.0" | ||||||
| @@ -9,13 +9,22 @@ pub const DEFAULT_ADMIN_PASSWORD: &str = "admin"; | |||||||
| pub const APP_NAME: &str = "Basic OIDC"; | pub const APP_NAME: &str = "Basic OIDC"; | ||||||
|  |  | ||||||
| /// Maximum session duration after inactivity, in seconds | /// Maximum session duration after inactivity, in seconds | ||||||
| pub const MAX_INACTIVITY_DURATION: u64 = 60 * 30; | pub const MAX_INACTIVITY_DURATION: i64 = 60 * 30; | ||||||
|  |  | ||||||
| /// Minimum interval between each last activity record in session | /// Maximum session duration (6 hours) | ||||||
| pub const MIN_ACTIVITY_RECORD_TIME: u64 = 10; | pub const MAX_SESSION_DURATION: i64 = 3600 * 6; | ||||||
|  |  | ||||||
| /// Minimum password length | /// Minimum password length | ||||||
| pub const MIN_PASS_LEN: usize = 4; | pub const MIN_PASS_LEN: usize = 4; | ||||||
|  |  | ||||||
| /// Maximum session duration (6 hours) | /// The name of the cookie used to store session information | ||||||
| pub const MAX_SESSION_DURATION: u64 = 3600 * 6; | pub const SESSION_COOKIE_NAME: &str = "auth-cookie"; | ||||||
|  |  | ||||||
|  | /// Authenticated routes prefix | ||||||
|  | pub const AUTHENTICATED_ROUTES: &str = "/settings"; | ||||||
|  |  | ||||||
|  | /// Admin routes prefix | ||||||
|  | pub const ADMIN_ROUTES: &str = "/admin"; | ||||||
|  |  | ||||||
|  | /// Auth route | ||||||
|  | pub const LOGIN_ROUTE: &str = "/login"; | ||||||
| @@ -1,8 +1,22 @@ | |||||||
|  | use std::fmt::Display; | ||||||
|  |  | ||||||
| use actix_web::HttpResponse; | use actix_web::HttpResponse; | ||||||
|  |  | ||||||
|  | use crate::constants::LOGIN_ROUTE; | ||||||
|  |  | ||||||
| /// Create a redirect user response | /// Create a redirect user response | ||||||
| pub fn redirect_user(uri: &str) -> HttpResponse { | pub fn redirect_user(uri: &str) -> HttpResponse { | ||||||
|     HttpResponse::Found() |     HttpResponse::Found() | ||||||
|         .append_header(("Location", uri)) |         .append_header(("Location", uri)) | ||||||
|         .finish() |         .finish() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// Redirect user to authenticate him | ||||||
|  | pub fn redirect_user_for_login<P: Display>(redirect_uri: P) -> HttpResponse { | ||||||
|  |     HttpResponse::Found() | ||||||
|  |         .append_header(( | ||||||
|  |             "Location", | ||||||
|  |             format!("{}?redirect={}", LOGIN_ROUTE, urlencoding::encode(&redirect_uri.to_string())) | ||||||
|  |         )) | ||||||
|  |         .finish() | ||||||
|  | } | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ struct BaseLoginPage { | |||||||
|     success: String, |     success: String, | ||||||
|     page_title: &'static str, |     page_title: &'static str, | ||||||
|     app_name: &'static str, |     app_name: &'static str, | ||||||
|  |     redirect_uri: String, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Template)] | #[derive(Template)] | ||||||
| @@ -41,6 +42,7 @@ pub struct LoginRequestBody { | |||||||
| #[derive(serde::Deserialize)] | #[derive(serde::Deserialize)] | ||||||
| pub struct LoginRequestQuery { | pub struct LoginRequestQuery { | ||||||
|     logout: Option<bool>, |     logout: Option<bool>, | ||||||
|  |     redirect: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Authenticate user | /// Authenticate user | ||||||
| @@ -52,6 +54,14 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | |||||||
|     let mut success = String::new(); |     let mut success = String::new(); | ||||||
|     let mut login = String::new(); |     let mut login = String::new(); | ||||||
|  |  | ||||||
|  |     let redirect_uri = match query.redirect.as_deref() { | ||||||
|  |         None => "/", | ||||||
|  |         Some(s) => match s.starts_with('/') && !s.starts_with("//") { | ||||||
|  |             true => s, | ||||||
|  |             false => "/", | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     // Check if user session must be closed |     // Check if user session must be closed | ||||||
|     if let Some(true) = query.logout { |     if let Some(true) = query.logout { | ||||||
|         id.forget(); |         id.forget(); | ||||||
| @@ -60,7 +70,7 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | |||||||
|  |  | ||||||
|     // Check if user is already authenticated |     // Check if user is already authenticated | ||||||
|     if SessionIdentity(&id).is_authenticated() { |     if SessionIdentity(&id).is_authenticated() { | ||||||
|         return redirect_user("/"); |         return redirect_user(redirect_uri); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Check if user is setting a new  password |     // Check if user is setting a new  password | ||||||
| @@ -78,7 +88,7 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | |||||||
|                 danger = "Failed to change password!".to_string(); |                 danger = "Failed to change password!".to_string(); | ||||||
|             } else { |             } else { | ||||||
|                 SessionIdentity(&id).set_status(SessionStatus::SignedIn); |                 SessionIdentity(&id).set_status(SessionStatus::SignedIn); | ||||||
|                 return redirect_user("/"); |                 return redirect_user(redirect_uri); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -100,7 +110,7 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | |||||||
|                 if user.need_reset_password { |                 if user.need_reset_password { | ||||||
|                     SessionIdentity(&id).set_status(SessionStatus::NeedNewPassword); |                     SessionIdentity(&id).set_status(SessionStatus::NeedNewPassword); | ||||||
|                 } else { |                 } else { | ||||||
|                     return redirect_user("/"); |                     return redirect_user(redirect_uri); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -122,6 +132,7 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | |||||||
|                     danger, |                     danger, | ||||||
|                     success, |                     success, | ||||||
|                     app_name: APP_NAME, |                     app_name: APP_NAME, | ||||||
|  |                     redirect_uri: urlencoding::encode(redirect_uri).to_string(), | ||||||
|                 }, |                 }, | ||||||
|                 min_pass_len: MIN_PASS_LEN, |                 min_pass_len: MIN_PASS_LEN, | ||||||
|             }.render().unwrap()); |             }.render().unwrap()); | ||||||
| @@ -136,6 +147,7 @@ pub async fn login_route(users: web::Data<Addr<UsersActor>>, | |||||||
|                 danger, |                 danger, | ||||||
|                 success, |                 success, | ||||||
|                 app_name: APP_NAME, |                 app_name: APP_NAME, | ||||||
|  |                 redirect_uri: urlencoding::encode(redirect_uri).to_string(), | ||||||
|             }, |             }, | ||||||
|             login, |             login, | ||||||
|         }.render().unwrap()) |         }.render().unwrap()) | ||||||
|   | |||||||
| @@ -1,9 +1,7 @@ | |||||||
| use actix_identity::Identity; | use actix_identity::Identity; | ||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
|  |  | ||||||
| use crate::constants::{MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, MIN_ACTIVITY_RECORD_TIME}; |  | ||||||
| use crate::data::user::{User, UserID}; | use crate::data::user::{User, UserID}; | ||||||
| use crate::utils::time::time; |  | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] | #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] | ||||||
| pub enum SessionStatus { | pub enum SessionStatus { | ||||||
| @@ -21,30 +19,21 @@ impl Default for SessionStatus { | |||||||
|  |  | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize, Default)] | #[derive(Debug, Serialize, Deserialize, Default)] | ||||||
| struct SessionIdentityData { | pub struct SessionIdentityData { | ||||||
|     pub id: UserID, |     pub id: UserID, | ||||||
|     pub is_admin: bool, |     pub is_admin: bool, | ||||||
|     login_time: u64, |  | ||||||
|     last_access: u64, |  | ||||||
|     pub status: SessionStatus, |     pub status: SessionStatus, | ||||||
| } | } | ||||||
|  |  | ||||||
| pub struct SessionIdentity<'a>(pub &'a Identity); | pub struct SessionIdentity<'a>(pub &'a Identity); | ||||||
|  |  | ||||||
| impl<'a> SessionIdentity<'a> { | impl<'a> SessionIdentity<'a> { | ||||||
|     pub fn set_user(&self, user: &User) { |     fn get_session_data(&self) -> Option<SessionIdentityData> { | ||||||
|         self.set_session_data(&SessionIdentityData { |         Self::deserialize_session_data(self.0.identity()) | ||||||
|             id: user.uid.clone(), |  | ||||||
|             is_admin: user.admin, |  | ||||||
|             login_time: time(), |  | ||||||
|             last_access: time(), |  | ||||||
|             status: SessionStatus::SignedIn, |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fn get_session_data(&self) -> Option<SessionIdentityData> { |     pub fn deserialize_session_data(data: Option<String>) -> Option<SessionIdentityData> { | ||||||
|         let mut res: Option<SessionIdentityData> = self.0.identity() |         let res: Option<SessionIdentityData> = data.as_deref() | ||||||
|             .as_deref() |  | ||||||
|             .map(serde_json::from_str) |             .map(serde_json::from_str) | ||||||
|             .map(|f| match f { |             .map(|f| match f { | ||||||
|                 Ok(d) => Some(d), |                 Ok(d) => Some(d), | ||||||
| @@ -62,26 +51,6 @@ impl<'a> SessionIdentity<'a> { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if let Some(session) = res.as_mut() { |  | ||||||
|             if session.login_time + MAX_SESSION_DURATION < time() { |  | ||||||
|                 log::info!("Session for {} reached max duration timeout", session.id); |  | ||||||
|                 self.0.forget(); |  | ||||||
|                 return None; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if session.last_access + MAX_INACTIVITY_DURATION < time() { |  | ||||||
|                 log::info!("Session is expired for {}", session.id); |  | ||||||
|                 self.0.forget(); |  | ||||||
|                 return None; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if session.last_access + MIN_ACTIVITY_RECORD_TIME < time() { |  | ||||||
|                 log::debug!("Refresh last access for session"); |  | ||||||
|                 session.last_access = time(); |  | ||||||
|                 self.set_session_data(session); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         res |         res | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -92,6 +61,14 @@ impl<'a> SessionIdentity<'a> { | |||||||
|         self.0.remember(s); |         self.0.remember(s); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub fn set_user(&self, user: &User) { | ||||||
|  |         self.set_session_data(&SessionIdentityData { | ||||||
|  |             id: user.uid.clone(), | ||||||
|  |             is_admin: user.admin, | ||||||
|  |             status: SessionStatus::SignedIn, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub fn set_status(&self, status: SessionStatus) { |     pub fn set_status(&self, status: SessionStatus) { | ||||||
|         let mut sess = self.get_session_data().unwrap_or_default(); |         let mut sess = self.get_session_data().unwrap_or_default(); | ||||||
|         sess.status = status; |         sess.status = status; | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,11 +1,13 @@ | |||||||
| use actix::Actor; | use actix::Actor; | ||||||
| use actix_identity::{CookieIdentityPolicy, IdentityService}; | use actix_identity::{CookieIdentityPolicy, IdentityService}; | ||||||
| use actix_web::{App, get, HttpServer, web}; | use actix_web::{App, get, HttpServer, web}; | ||||||
|  | use actix_web::cookie::SameSite; | ||||||
|  | use actix_web::cookie::time::Duration; | ||||||
| use actix_web::middleware::Logger; | use actix_web::middleware::Logger; | ||||||
| use clap::Parser; | use clap::Parser; | ||||||
|  |  | ||||||
| use basic_oidc::actors::users_actor::UsersActor; | use basic_oidc::actors::users_actor::UsersActor; | ||||||
| use basic_oidc::constants::{DEFAULT_ADMIN_PASSWORD, DEFAULT_ADMIN_USERNAME}; | use basic_oidc::constants::{DEFAULT_ADMIN_PASSWORD, DEFAULT_ADMIN_USERNAME, MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME}; | ||||||
| use basic_oidc::controllers::assets_controller::assets_route; | use basic_oidc::controllers::assets_controller::assets_route; | ||||||
| use basic_oidc::controllers::login_controller::{login_route, logout_route}; | use basic_oidc::controllers::login_controller::{login_route, logout_route}; | ||||||
| use basic_oidc::data::app_config::AppConfig; | use basic_oidc::data::app_config::AppConfig; | ||||||
| @@ -63,16 +65,19 @@ async fn main() -> std::io::Result<()> { | |||||||
|  |  | ||||||
|     HttpServer::new(move || { |     HttpServer::new(move || { | ||||||
|         let policy = CookieIdentityPolicy::new(config.token_key.as_bytes()) |         let policy = CookieIdentityPolicy::new(config.token_key.as_bytes()) | ||||||
|             .name("auth-cookie") |             .name(SESSION_COOKIE_NAME) | ||||||
|             .secure(config.secure_auth_cookie); |             .secure(config.secure_auth_cookie) | ||||||
|  |             .visit_deadline(Duration::seconds(MAX_INACTIVITY_DURATION)) | ||||||
|  |             .login_deadline(Duration::seconds(MAX_SESSION_DURATION)) | ||||||
|  |             .same_site(SameSite::Strict); | ||||||
|  |  | ||||||
|  |  | ||||||
|         App::new() |         App::new() | ||||||
|             .app_data(web::Data::new(users_actor.clone())) |             .app_data(web::Data::new(users_actor.clone())) | ||||||
|  |  | ||||||
|             .wrap(Logger::default()) |             .wrap(Logger::default()) | ||||||
|             .wrap(IdentityService::new(policy)) |  | ||||||
|             .wrap(AuthMiddleware {}) |             .wrap(AuthMiddleware {}) | ||||||
|  |             .wrap(IdentityService::new(policy)) | ||||||
|  |  | ||||||
|             // /health route |             // /health route | ||||||
|             .service(health) |             .service(health) | ||||||
|   | |||||||
| @@ -4,8 +4,14 @@ use std::future::{Future, ready, Ready}; | |||||||
| use std::pin::Pin; | use std::pin::Pin; | ||||||
| use std::rc::Rc; | use std::rc::Rc; | ||||||
|  |  | ||||||
|  | use actix_identity::RequestIdentity; | ||||||
| use actix_web::{dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpResponse}; | use actix_web::{dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, Error, HttpResponse}; | ||||||
| use actix_web::body::EitherBody; | use actix_web::body::EitherBody; | ||||||
|  | use askama::Template; | ||||||
|  |  | ||||||
|  | use crate::constants::{ADMIN_ROUTES, AUTHENTICATED_ROUTES}; | ||||||
|  | use crate::controllers::base_controller::redirect_user_for_login; | ||||||
|  | use crate::data::session_identity::{SessionIdentity, SessionIdentityData}; | ||||||
|  |  | ||||||
| // There are two steps in middleware processing. | // There are two steps in middleware processing. | ||||||
| // 1. Middleware initialization, middleware factory gets called with | // 1. Middleware initialization, middleware factory gets called with | ||||||
| @@ -33,6 +39,27 @@ impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug)] | ||||||
|  | enum SessionStatus { | ||||||
|  |     SignedOut, | ||||||
|  |     RegularUser, | ||||||
|  |     Admin, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl SessionStatus { | ||||||
|  |     pub fn is_auth(&self) -> bool { | ||||||
|  |         !matches!(self, SessionStatus::SignedOut) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn is_admin(&self) -> bool { | ||||||
|  |         matches!(self, SessionStatus::Admin) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Template)] | ||||||
|  | #[template(path = "access_denied.html")] | ||||||
|  | struct AccessDeniedTemplate {} | ||||||
|  |  | ||||||
| pub struct AuthInnerMiddleware<S> { | pub struct AuthInnerMiddleware<S> { | ||||||
|     service: Rc<S>, |     service: Rc<S>, | ||||||
| } | } | ||||||
| @@ -45,13 +72,13 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S> | |||||||
| { | { | ||||||
|     type Response = ServiceResponse<EitherBody<B>>; |     type Response = ServiceResponse<EitherBody<B>>; | ||||||
|     type Error = Error; |     type Error = Error; | ||||||
|  |  | ||||||
|  |     #[allow(clippy::type_complexity)] | ||||||
|     type Future = Pin<Box<dyn Future<Output=Result<Self::Response, Self::Error>>>>; |     type Future = Pin<Box<dyn Future<Output=Result<Self::Response, Self::Error>>>>; | ||||||
|  |  | ||||||
|     forward_ready!(service); |     forward_ready!(service); | ||||||
|  |  | ||||||
|     fn call(&self, req: ServiceRequest) -> Self::Future { |     fn call(&self, req: ServiceRequest) -> Self::Future { | ||||||
|         println!("Hi from start. You requested: {}", req.path()); |  | ||||||
|  |  | ||||||
|         let service = Rc::clone(&self.service); |         let service = Rc::clone(&self.service); | ||||||
|  |  | ||||||
|         // Forward request |         // Forward request | ||||||
| @@ -64,6 +91,27 @@ impl<S, B> Service<ServiceRequest> for AuthInnerMiddleware<S> | |||||||
|                 )); |                 )); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             let identity = match SessionIdentity::deserialize_session_data(req.get_identity()) { | ||||||
|  |                 None => SessionStatus::SignedOut, | ||||||
|  |                 Some(SessionIdentityData { is_admin: true, .. }) => SessionStatus::Admin, | ||||||
|  |                 _ => SessionStatus::RegularUser, | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             // Redirect user to login page | ||||||
|  |             if !identity.is_auth() && (req.path().starts_with(ADMIN_ROUTES) || | ||||||
|  |                 req.path().starts_with(AUTHENTICATED_ROUTES)) { | ||||||
|  |                 let path = req.path().to_string(); | ||||||
|  |                 return Ok(req.into_response(redirect_user_for_login(path)) | ||||||
|  |                     .map_into_right_body()); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // Restrict access to admin pages | ||||||
|  |             if !identity.is_admin() && req.path().starts_with(ADMIN_ROUTES) { | ||||||
|  |                 return Ok(req.into_response(HttpResponse::Unauthorized() | ||||||
|  |                     .body(AccessDeniedTemplate {}.render().unwrap())) | ||||||
|  |                     .map_into_right_body()); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             service |             service | ||||||
|                 .call(req) |                 .call(req) | ||||||
|                 .await |                 .await | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								templates/access_denied.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								templates/access_denied.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  |     <meta charset="UTF-8"> | ||||||
|  |     <title>Access denied</title> | ||||||
|  |  | ||||||
|  |     <link href="/assets/css/bootstrap.css" rel="stylesheet" crossorigin="anonymous"/> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |     <p>You are not allowed to access this resource.</p> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| {% extends "base_login_page.html" %} | {% extends "base_login_page.html" %} | ||||||
| {% block content %} | {% block content %} | ||||||
| <form action="/login" method="post"> | <form action="/login?redirect={{ redirect_uri }}" method="post"> | ||||||
|     <div> |     <div> | ||||||
|         <div class="form-floating"> |         <div class="form-floating"> | ||||||
|             <input name="login" type="text" required class="form-control" id="floatingName" placeholder="unsername" |             <input name="login" type="text" required class="form-control" id="floatingName" placeholder="unsername" | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| {% extends "base_login_page.html" %} | {% extends "base_login_page.html" %} | ||||||
| {% block content %} | {% block content %} | ||||||
| <form action="/login" method="post" id="reset_password_form"> | <form action="/login?redirect={{ redirect_uri }}" method="post" id="reset_password_form"> | ||||||
|     <div> |     <div> | ||||||
|         <p>You need to configure a new password:</p> |         <p>You need to configure a new password:</p> | ||||||
|  |  | ||||||
| @@ -11,13 +11,13 @@ | |||||||
|  |  | ||||||
|         <div class="form-floating"> |         <div class="form-floating"> | ||||||
|             <input name="password" type="password" required class="form-control" id="pass1" |             <input name="password" type="password" required class="form-control" id="pass1" | ||||||
|                    placeholder="unsername"/> |                    placeholder="Password"/> | ||||||
|             <label for="pass1">New password</label> |             <label for="pass1">New password</label> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div class="form-floating"> |         <div class="form-floating"> | ||||||
|             <input type="password" required class="form-control" id="pass2" |             <input type="password" required class="form-control" id="pass2" | ||||||
|                    placeholder="Password"/> |                    placeholder="Password confirmation"/> | ||||||
|             <label for="pass2">Confirm new password</label> |             <label for="pass2">Confirm new password</label> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
| @@ -47,6 +47,7 @@ | |||||||
|             form.submit(); |             form.submit(); | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|  |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user