forgot password: email and paper key, reset password
This commit is contained in:
parent
6ddbe013bc
commit
751a21485f
21 changed files with 688 additions and 4 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3279,6 +3279,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"database",
|
"database",
|
||||||
"email_address",
|
"email_address",
|
||||||
|
"gen_passphrase",
|
||||||
"hash",
|
"hash",
|
||||||
"id",
|
"id",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ serde_json = "1"
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
nanoid-dictionary = "0.4"
|
nanoid-dictionary = "0.4"
|
||||||
email_address = { version = "0.2", default-features = false }
|
email_address = { version = "0.2", default-features = false }
|
||||||
|
gen_passphrase = "0.1.1"
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package.sqlx-macros]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
select id,
|
||||||
|
created_at as "created_at: DateTime<Utc>",
|
||||||
|
updated_at as "updated_at: DateTime<Utc>",
|
||||||
|
is_admin as "is_admin: bool",
|
||||||
|
username,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
password_recover,
|
||||||
|
paper_key,
|
||||||
|
is_archived as "is_archived: bool",
|
||||||
|
timezone
|
||||||
|
from users
|
||||||
|
|
||||||
|
where password_recover is (?)
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
update users
|
||||||
|
|
||||||
|
set password_recover = ?
|
||||||
|
|
||||||
|
where id is ?
|
||||||
|
|
@ -110,6 +110,16 @@
|
||||||
},
|
},
|
||||||
"query": "update apps\n\nset is_archived = 1\n\nwhere id is ?"
|
"query": "update apps\n\nset is_archived = 1\n\nwhere id is ?"
|
||||||
},
|
},
|
||||||
|
"32d35bdd1f4cf64ce0ff7beb7a11591e0f35eab7211692bcde8230c68e4cedf3": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "update users\n\nset password_recover = ?\n\nwhere id is ?"
|
||||||
|
},
|
||||||
"35de1a35e6cf6c683a1b2ca3605791aea9cbb852ac1d3df151cc21c341046361": {
|
"35de1a35e6cf6c683a1b2ca3605791aea9cbb852ac1d3df151cc21c341046361": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|
@ -284,6 +294,90 @@
|
||||||
},
|
},
|
||||||
"query": "update users\n\nset username = ?\n\nwhere id is ?"
|
"query": "update users\n\nset username = ?\n\nwhere id is ?"
|
||||||
},
|
},
|
||||||
|
"56a88e7e68cfa94a055008510e3bc4389d7a7f64b43479d5fc8e4495ade0f84a": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at: DateTime<Utc>",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at: DateTime<Utc>",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "is_admin: bool",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password_recover",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "paper_key",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "is_archived: bool",
|
||||||
|
"ordinal": 10,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timezone",
|
||||||
|
"ordinal": 11,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "select id,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n is_admin as \"is_admin: bool\",\n username,\n name,\n email,\n password,\n password_recover,\n paper_key,\n is_archived as \"is_archived: bool\",\n timezone\nfrom users\n\nwhere password_recover is (?)\n"
|
||||||
|
},
|
||||||
"56a9c0dff010858189a95087d014c7d0ce930da5d841b9d788a9c0e84b580bc6": {
|
"56a9c0dff010858189a95087d014c7d0ce930da5d841b9d788a9c0e84b580bc6": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,20 @@ impl Users {
|
||||||
.map_err(handle_error)
|
.map_err(handle_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_one_from_password_reset_token(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
token: &str,
|
||||||
|
) -> Result<Option<Self>, Error> {
|
||||||
|
sqlx::query_file_as!(
|
||||||
|
Self,
|
||||||
|
"queries/users/get_one_from_password_reset_token.sql",
|
||||||
|
token
|
||||||
|
)
|
||||||
|
.fetch_optional(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result<Vec<Self>, Error> {
|
pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result<Vec<Self>, Error> {
|
||||||
sqlx::query_file_as!(Self, "queries/users/get_all.sql")
|
sqlx::query_file_as!(Self, "queries/users/get_all.sql")
|
||||||
.fetch_all(conn)
|
.fetch_all(conn)
|
||||||
|
|
@ -185,4 +199,18 @@ impl Users {
|
||||||
|
|
||||||
Ok((query.rows_affected() == 1).then_some(()))
|
Ok((query.rows_affected() == 1).then_some(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_password_reset_token(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
id: &str,
|
||||||
|
token: Option<&str>,
|
||||||
|
) -> Result<Option<()>, Error> {
|
||||||
|
let query: SqliteQueryResult =
|
||||||
|
sqlx::query_file!("queries/users/set_password_reset_token.sql", token, id)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)?;
|
||||||
|
|
||||||
|
Ok((query.rows_affected() == 1).then_some(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ mod completed_setup;
|
||||||
mod jwt;
|
mod jwt;
|
||||||
mod need_setup;
|
mod need_setup;
|
||||||
mod refresh_token;
|
mod refresh_token;
|
||||||
|
mod reset_password_token;
|
||||||
|
|
||||||
pub use self::jwt::*;
|
pub use self::jwt::*;
|
||||||
pub use access_token::AccessToken;
|
pub use access_token::AccessToken;
|
||||||
|
|
@ -11,3 +12,4 @@ pub use basic_auth::BasicAuth;
|
||||||
pub use completed_setup::CompletedSetup;
|
pub use completed_setup::CompletedSetup;
|
||||||
pub use need_setup::NeedSetup;
|
pub use need_setup::NeedSetup;
|
||||||
pub use refresh_token::RefreshToken;
|
pub use refresh_token::RefreshToken;
|
||||||
|
pub use reset_password_token::RocketResetPasswordToken;
|
||||||
36
crates/ezidam/src/guards/reset_password_token.rs
Normal file
36
crates/ezidam/src/guards/reset_password_token.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use rocket::form;
|
||||||
|
use rocket::form::{FromFormField, ValueField};
|
||||||
|
use rocket::http::impl_from_uri_param_identity;
|
||||||
|
use rocket::http::uri::fmt::{Formatter, Query, UriDisplay};
|
||||||
|
use rocket::request::FromParam;
|
||||||
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
use users::password_reset::{Error, PasswordResetToken};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
pub struct RocketResetPasswordToken(pub PasswordResetToken);
|
||||||
|
|
||||||
|
impl<'r> FromParam<'r> for RocketResetPasswordToken {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
||||||
|
PasswordResetToken::parse(param).map(Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UriDisplay<Query> for RocketResetPasswordToken {
|
||||||
|
fn fmt(&self, f: &mut Formatter<Query>) -> fmt::Result {
|
||||||
|
UriDisplay::fmt(&self.0.to_string(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl_from_uri_param_identity!([Query] RocketResetPasswordToken);
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromFormField<'r> for RocketResetPasswordToken {
|
||||||
|
fn from_value(field: ValueField<'r>) -> form::Result<'r, Self> {
|
||||||
|
Ok(PasswordResetToken::parse(field.value)
|
||||||
|
.map(Self)
|
||||||
|
.map_err(|e| form::Error::validation(e.to_string()))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,8 +40,11 @@ impl Icon {
|
||||||
"id-badge-2", IdBadge2, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-id-badge-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 12h3v4h-3z"></path><path d="M10 6h-6a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h16a1 1 0 0 0 1 -1v-12a1 1 0 0 0 -1 -1h-6"></path><path d="M10 3m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 16h2"></path><path d="M14 12h4"></path></svg>"#,
|
"id-badge-2", IdBadge2, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-id-badge-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 12h3v4h-3z"></path><path d="M10 6h-6a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h16a1 1 0 0 0 1 -1v-12a1 1 0 0 0 -1 -1h-6"></path><path d="M10 3m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 16h2"></path><path d="M14 12h4"></path></svg>"#,
|
||||||
"user", User, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-user" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"></path><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path></svg>"#,
|
"user", User, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-user" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"></path><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path></svg>"#,
|
||||||
"at", At, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-at" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M16 12v1.5a2.5 2.5 0 0 0 5 0v-1.5a9 9 0 1 0 -5.5 8.28"></path></svg>"#,
|
"at", At, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-at" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M16 12v1.5a2.5 2.5 0 0 0 5 0v-1.5a9 9 0 1 0 -5.5 8.28"></path></svg>"#,
|
||||||
|
"paperclip", Paperclip, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-paperclip" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path></svg>"#,
|
||||||
"paperclip-large", PaperclipLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-paperclip" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path></svg>"#,
|
"paperclip-large", PaperclipLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-paperclip" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path></svg>"#,
|
||||||
"users", Users, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path><path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path></svg>"#
|
"users", Users, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path><path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path></svg>"#,
|
||||||
|
"mail", Mail, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-mail" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"></path><path d="M3 7l9 6l9 -6"></path></svg>"#,
|
||||||
|
"password", Password, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-password" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 10v4"></path><path d="M10 13l4 -2"></path><path d="M10 11l4 2"></path><path d="M5 10v4"></path><path d="M3 13l4 -2"></path><path d="M3 11l4 2"></path><path d="M19 10v4"></path><path d="M17 13l4 -2"></path><path d="M17 11l4 2"></path></svg>"#
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,8 +65,11 @@ pub fn icons_to_templates(tera: &mut Tera) {
|
||||||
Icon::IdBadge2,
|
Icon::IdBadge2,
|
||||||
Icon::User,
|
Icon::User,
|
||||||
Icon::At,
|
Icon::At,
|
||||||
|
Icon::Paperclip,
|
||||||
Icon::PaperclipLarge,
|
Icon::PaperclipLarge,
|
||||||
Icon::Users,
|
Icon::Users,
|
||||||
|
Icon::Mail,
|
||||||
|
Icon::Password,
|
||||||
];
|
];
|
||||||
|
|
||||||
// For each icon, it will output: ("icons/name", "<svg>...</svg>")
|
// For each icon, it will output: ("icons/name", "<svg>...</svg>")
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ pub enum Page {
|
||||||
UserSecuritySettings(UserSecuritySettings),
|
UserSecuritySettings(UserSecuritySettings),
|
||||||
UserVisualSettings(UserVisualSettings),
|
UserVisualSettings(UserVisualSettings),
|
||||||
AdminUsersList(AdminUsersList),
|
AdminUsersList(AdminUsersList),
|
||||||
|
ForgotPassword,
|
||||||
|
ResetPassword(ResetPassword),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
|
|
@ -46,6 +48,8 @@ impl Page {
|
||||||
Page::UserSecuritySettings(_) => "pages/settings/security",
|
Page::UserSecuritySettings(_) => "pages/settings/security",
|
||||||
Page::UserVisualSettings(_) => "pages/settings/visual",
|
Page::UserVisualSettings(_) => "pages/settings/visual",
|
||||||
Page::AdminUsersList(_) => "pages/admin/users/list",
|
Page::AdminUsersList(_) => "pages/admin/users/list",
|
||||||
|
Page::ForgotPassword => "pages/forgot-password",
|
||||||
|
Page::ResetPassword(_) => "pages/reset-password",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,6 +71,8 @@ impl Page {
|
||||||
Page::UserSecuritySettings(_) => "Security settings",
|
Page::UserSecuritySettings(_) => "Security settings",
|
||||||
Page::UserVisualSettings(_) => "Visual settings",
|
Page::UserVisualSettings(_) => "Visual settings",
|
||||||
Page::AdminUsersList(_) => "Users",
|
Page::AdminUsersList(_) => "Users",
|
||||||
|
Page::ForgotPassword => "Forgot password",
|
||||||
|
Page::ResetPassword(_) => "Reset password",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +96,8 @@ impl Page {
|
||||||
Page::UserSecuritySettings(_) => Some(UserMenu::Settings.into()),
|
Page::UserSecuritySettings(_) => Some(UserMenu::Settings.into()),
|
||||||
Page::UserVisualSettings(_) => Some(UserMenu::Settings.into()),
|
Page::UserVisualSettings(_) => Some(UserMenu::Settings.into()),
|
||||||
Page::AdminUsersList(_) => Some(AdminMenu::Users.into()),
|
Page::AdminUsersList(_) => Some(AdminMenu::Users.into()),
|
||||||
|
Page::ForgotPassword => None,
|
||||||
|
Page::ResetPassword(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,6 +119,8 @@ impl Page {
|
||||||
Page::UserSecuritySettings(security) => Box::new(security),
|
Page::UserSecuritySettings(security) => Box::new(security),
|
||||||
Page::UserVisualSettings(visual) => Box::new(visual),
|
Page::UserVisualSettings(visual) => Box::new(visual),
|
||||||
Page::AdminUsersList(list) => Box::new(list),
|
Page::AdminUsersList(list) => Box::new(list),
|
||||||
|
Page::ForgotPassword => Box::new(()),
|
||||||
|
Page::ResetPassword(reset) => Box::new(reset),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
use super::prelude::*;
|
use super::prelude::*;
|
||||||
|
use forgot_password::*;
|
||||||
use home::*;
|
use home::*;
|
||||||
use logo::*;
|
use logo::*;
|
||||||
use logout::*;
|
use logout::*;
|
||||||
|
use reset_password::*;
|
||||||
|
|
||||||
|
pub mod forgot_password;
|
||||||
pub mod home;
|
pub mod home;
|
||||||
pub mod logo;
|
pub mod logo;
|
||||||
pub mod logout;
|
pub mod logout;
|
||||||
|
pub mod reset_password;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![
|
||||||
|
|
@ -15,6 +19,11 @@ pub fn routes() -> Vec<Route> {
|
||||||
homepage_redirect,
|
homepage_redirect,
|
||||||
redirect_to_setup,
|
redirect_to_setup,
|
||||||
request_logout,
|
request_logout,
|
||||||
|
forgot_password_page,
|
||||||
|
forgot_password_email_form,
|
||||||
|
forgot_password_paper_key_form,
|
||||||
|
reset_password_page,
|
||||||
|
reset_password_form,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,4 +37,11 @@ pub mod content {
|
||||||
pub struct Homepage {
|
pub struct Homepage {
|
||||||
pub user: JwtClaims,
|
pub user: JwtClaims,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ResetPassword {
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
150
crates/ezidam/src/routes/root/forgot_password.rs
Normal file
150
crates/ezidam/src/routes/root/forgot_password.rs
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
use crate::routes::prelude::*;
|
||||||
|
use email_address::EmailAddress;
|
||||||
|
use hash::PaperKey;
|
||||||
|
use rocket::{get, post};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use users::{password_reset::PasswordResetToken, User};
|
||||||
|
|
||||||
|
#[get("/forgot-password")]
|
||||||
|
pub async fn forgot_password_page(flash: Option<FlashMessage<'_>>) -> Template {
|
||||||
|
let page = Page::ForgotPassword;
|
||||||
|
|
||||||
|
flash
|
||||||
|
.map(|flash| Page::with_flash(page.clone(), flash))
|
||||||
|
.unwrap_or_else(|| page.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromForm)]
|
||||||
|
pub struct ForgotPasswordEmailForm<'r> {
|
||||||
|
pub email: &'r str,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUCCESS_MESSAGE: &str = "An email is on the way with instructions to reset your password.";
|
||||||
|
|
||||||
|
#[post("/forgot-password/email", data = "<form>")]
|
||||||
|
pub async fn forgot_password_email_form(
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
form: Form<ForgotPasswordEmailForm<'_>>,
|
||||||
|
) -> Result<Flash<Redirect>> {
|
||||||
|
if form.email.is_empty() {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(forgot_password_page)),
|
||||||
|
FlashKind::Danger,
|
||||||
|
"Please enter an email address",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse email address
|
||||||
|
let email = match EmailAddress::from_str(form.email) {
|
||||||
|
Ok(email) => email,
|
||||||
|
Err(e) => {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(forgot_password_page)),
|
||||||
|
FlashKind::Danger,
|
||||||
|
e.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
let user = match User::get_by_email(&mut transaction, &email).await? {
|
||||||
|
Some(user) => user,
|
||||||
|
None => {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(forgot_password_page)),
|
||||||
|
FlashKind::Success,
|
||||||
|
SUCCESS_MESSAGE,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate reset token
|
||||||
|
let token = task::spawn_blocking(|| PasswordResetToken::generate(15)).await?;
|
||||||
|
|
||||||
|
// Save in database
|
||||||
|
user.set_password_reset_token(&mut transaction, Some(&token))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
// TODO: send email here
|
||||||
|
|
||||||
|
Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(forgot_password_page)),
|
||||||
|
FlashKind::Success,
|
||||||
|
SUCCESS_MESSAGE,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromForm)]
|
||||||
|
pub struct ForgotPasswordPaperKeyForm<'r> {
|
||||||
|
pub login: &'r str,
|
||||||
|
pub paper_key: &'r str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/forgot-password/paper-key", data = "<form>")]
|
||||||
|
pub async fn forgot_password_paper_key_form(
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
form: Form<ForgotPasswordPaperKeyForm<'_>>,
|
||||||
|
) -> Result<Flash<Redirect>> {
|
||||||
|
if form.login.is_empty() || form.paper_key.is_empty() {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(forgot_password_page)),
|
||||||
|
FlashKind::Danger,
|
||||||
|
"Please fill out the form",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
let user = match User::get_by_login(&mut transaction, form.login).await? {
|
||||||
|
Some(user) => user,
|
||||||
|
None => {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(forgot_password_page)),
|
||||||
|
FlashKind::Danger,
|
||||||
|
"User not found",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let paper_key = match user.paper_key_hashed() {
|
||||||
|
Some(paper_key) => PaperKey::from_hash(paper_key),
|
||||||
|
None => {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(forgot_password_page)),
|
||||||
|
FlashKind::Danger,
|
||||||
|
"Paper key not found",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify paper key
|
||||||
|
let paper_key_input = form.paper_key.to_string();
|
||||||
|
if !task::spawn_blocking(move || paper_key.compare(&paper_key_input)).await?? {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(forgot_password_page)),
|
||||||
|
FlashKind::Danger,
|
||||||
|
"Invalid paper key",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate reset token
|
||||||
|
let token = task::spawn_blocking(|| PasswordResetToken::generate(15)).await?;
|
||||||
|
|
||||||
|
// Save in database
|
||||||
|
user.set_password_reset_token(&mut transaction, Some(&token))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
let token = RocketResetPasswordToken(token);
|
||||||
|
Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(super::reset_password_page(token))),
|
||||||
|
FlashKind::Success,
|
||||||
|
SUCCESS_MESSAGE,
|
||||||
|
))
|
||||||
|
}
|
||||||
81
crates/ezidam/src/routes/root/reset_password.rs
Normal file
81
crates/ezidam/src/routes/root/reset_password.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
use crate::routes::prelude::*;
|
||||||
|
use rocket::{get, post};
|
||||||
|
use users::User;
|
||||||
|
|
||||||
|
#[get("/reset-password?<token>")]
|
||||||
|
pub async fn reset_password_page(
|
||||||
|
token: RocketResetPasswordToken,
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
flash: Option<FlashMessage<'_>>,
|
||||||
|
) -> Result<Template> {
|
||||||
|
if token.0.has_expired() {
|
||||||
|
return Err(Error::bad_request("Reset password token has expired"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = User::get_one_from_password_reset_token(&mut **db, &token.0)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found("Failed to find user from token"))?;
|
||||||
|
|
||||||
|
let page = Page::ResetPassword(super::content::ResetPassword {
|
||||||
|
username: user.username().into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(flash
|
||||||
|
.map(|flash| Page::with_flash(page.clone(), flash))
|
||||||
|
.unwrap_or_else(|| page.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromForm)]
|
||||||
|
pub struct ResetPasswordForm<'r> {
|
||||||
|
pub password: &'r str,
|
||||||
|
pub confirm_password: &'r str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/reset-password?<token>", data = "<form>")]
|
||||||
|
pub async fn reset_password_form(
|
||||||
|
token: RocketResetPasswordToken,
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
form: Form<ResetPasswordForm<'_>>,
|
||||||
|
) -> Result<Flash<Redirect>> {
|
||||||
|
if form.password != form.confirm_password {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(reset_password_page(token))),
|
||||||
|
FlashKind::Danger,
|
||||||
|
"Password do not match",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.password.is_empty() {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(reset_password_page(token))),
|
||||||
|
FlashKind::Danger,
|
||||||
|
"Empty password",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
let password = form.password.to_string();
|
||||||
|
let password = task::spawn_blocking(move || Password::new(&password)).await??;
|
||||||
|
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Get user info
|
||||||
|
let user = User::get_one_from_password_reset_token(&mut transaction, &token.0)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found("Could not find user"))?;
|
||||||
|
|
||||||
|
// Set password
|
||||||
|
user.set_password(&mut transaction, Some(&password)).await?;
|
||||||
|
|
||||||
|
// Consume password reset token
|
||||||
|
user.set_password_reset_token(&mut transaction, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(crate::routes::oauth::authorize::authorize_ezidam)),
|
||||||
|
FlashKind::Success,
|
||||||
|
"Your new password has been saved.",
|
||||||
|
))
|
||||||
|
}
|
||||||
96
crates/ezidam/templates/pages/forgot-password.html.tera
Normal file
96
crates/ezidam/templates/pages/forgot-password.html.tera
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<body class=" d-flex flex-column">
|
||||||
|
<script src="/js/demo-theme.min.js"></script>
|
||||||
|
<div>
|
||||||
|
<div class="min-vh-100 d-flex flex-column justify-content-between">
|
||||||
|
<div class="container container-tight py-4">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
{% include "utils/logo" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if flash %}
|
||||||
|
<div class="alert alert-{{flash.0}}" role="alert">
|
||||||
|
<h4 class="alert-title">{{ flash.1 }}</h4>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-title text-center my-4 h2">Reset your password</h2>
|
||||||
|
<div class="card-header">
|
||||||
|
<ul class="nav nav-tabs card-header-tabs nav-fill" data-bs-toggle="tabs">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#tabs-email" class="nav-link active" data-bs-toggle="tab">Email</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#tabs-paper-key" class="nav-link" data-bs-toggle="tab">Paper key</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane active show" id="tabs-email">
|
||||||
|
<p class="mt-2 mb-4">
|
||||||
|
Enter your email address linked to your account. We will email you a link to reset your
|
||||||
|
password.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form class="mb-2" action="/forgot-password/email" method="post" autocomplete="off"
|
||||||
|
novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label required" for="email">Email address</label>
|
||||||
|
<input id="email" type="email" name="email" class="form-control"
|
||||||
|
placeholder="Enter email"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
<div class="form-footer">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
{% include "icons/mail" %}
|
||||||
|
Request password reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane" id="tabs-paper-key">
|
||||||
|
<p class="mt-2 mb-4">
|
||||||
|
Enter your login linked to your account, with your paper key.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form class="mb-2" action="/forgot-password/paper-key" method="post" autocomplete="off"
|
||||||
|
novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label required" for="login">Login</label>
|
||||||
|
<input id="login" type="text" name="login" class="form-control"
|
||||||
|
placeholder="Email or username"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label required" for="paper_key">Paper key</label>
|
||||||
|
<input id="paper_key" type="text" name="paper_key" class="form-control"
|
||||||
|
placeholder="Enter your paper key"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
<div class="form-footer">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
{% include "icons/paperclip" %}
|
||||||
|
Request password reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include "shell/footer" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Libs JS -->
|
||||||
|
<!-- Tabler Core -->
|
||||||
|
<script src="/js/tabler.min.js" defer></script>
|
||||||
|
<script src="/js/demo.min.js" defer></script>
|
||||||
|
</body>
|
||||||
|
{% endblock page %}
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
{% if user %}
|
{% if user %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center text-muted mt-3">
|
<div class="text-center text-muted mt-3">
|
||||||
<a href="./sign-up.html" tabindex="-1">Reset password</a>
|
<a href="/forgot-password" tabindex="-1">Forgot your password?</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
56
crates/ezidam/templates/pages/reset-password.html.tera
Normal file
56
crates/ezidam/templates/pages/reset-password.html.tera
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
{% extends "base" %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
<body class=" d-flex flex-column">
|
||||||
|
<script src="/js/demo-theme.min.js"></script>
|
||||||
|
<div>
|
||||||
|
<div class="min-vh-100 d-flex flex-column justify-content-between">
|
||||||
|
<div class="container container-tight py-4">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
{% include "utils/logo" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if flash %}
|
||||||
|
<div class="alert alert-{{flash.0}}" role="alert">
|
||||||
|
<h4 class="alert-title">{{ flash.1 }}</h4>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-title text-center my-4 h2">Reset your password</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<p class="mb-4 text-center">Resetting password for <code>{{ username }}</code></p>
|
||||||
|
|
||||||
|
<form class="mb-2" action="" method="post" autocomplete="off" novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label required" for="password">New password</label>
|
||||||
|
<input name="password" id="password" type="password" class="form-control" placeholder="Enter new password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label required" for="confirm_password">Confirm new password</label>
|
||||||
|
<input name="confirm_password" id="confirm_password" type="password" class="form-control" placeholder="Confirm new password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-footer">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
{% include "icons/password" %}
|
||||||
|
Set new password
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% include "shell/footer" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Libs JS -->
|
||||||
|
<!-- Tabler Core -->
|
||||||
|
<script src="/js/tabler.min.js" defer></script>
|
||||||
|
<script src="/js/demo.min.js" defer></script>
|
||||||
|
</body>
|
||||||
|
{% endblock page %}
|
||||||
|
|
@ -9,4 +9,4 @@ rand_core = { version = "0.6", features = ["std"] }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
nanoid = { workspace = true }
|
nanoid = { workspace = true }
|
||||||
nanoid-dictionary = { workspace = true }
|
nanoid-dictionary = { workspace = true }
|
||||||
gen_passphrase = { version = "0.1", features = ["eff_short_2"] }
|
gen_passphrase = { workspace = true, features = ["eff_short_2"] }
|
||||||
|
|
@ -11,3 +11,4 @@ thiserror = { workspace = true }
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
email_address = { workspace = true }
|
email_address = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
gen_passphrase = { workspace = true, features = ["eff_large"] }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
use crate::password_reset::PasswordResetToken;
|
||||||
use crate::User;
|
use crate::User;
|
||||||
use database::sqlx::SqliteExecutor;
|
use database::sqlx::SqliteExecutor;
|
||||||
use database::Error as DatabaseError;
|
use database::Error as DatabaseError;
|
||||||
|
|
@ -53,7 +54,7 @@ impl User {
|
||||||
.map(Self::from))
|
.map(Self::from))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_by_email(
|
pub async fn get_by_email(
|
||||||
conn: impl SqliteExecutor<'_>,
|
conn: impl SqliteExecutor<'_>,
|
||||||
email: &EmailAddress,
|
email: &EmailAddress,
|
||||||
) -> Result<Option<Self>, Error> {
|
) -> Result<Option<Self>, Error> {
|
||||||
|
|
@ -108,6 +109,17 @@ impl User {
|
||||||
.map(Self::from))
|
.map(Self::from))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_one_from_password_reset_token(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
token: &PasswordResetToken,
|
||||||
|
) -> Result<Option<Self>, Error> {
|
||||||
|
Ok(
|
||||||
|
DatabaseUsers::get_one_from_password_reset_token(conn, token.to_string().as_str())
|
||||||
|
.await?
|
||||||
|
.map(Self::from),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result<Vec<Self>, Error> {
|
pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result<Vec<Self>, Error> {
|
||||||
Ok(DatabaseUsers::get_all(conn)
|
Ok(DatabaseUsers::get_all(conn)
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -198,4 +210,19 @@ impl User {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_password_reset_token(
|
||||||
|
&self,
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
token: Option<&PasswordResetToken>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
DatabaseUsers::set_password_reset_token(
|
||||||
|
conn,
|
||||||
|
self.id.as_ref(),
|
||||||
|
token.map(|t| t.to_string()).as_deref(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
mod database;
|
mod database;
|
||||||
mod error;
|
mod error;
|
||||||
|
pub mod password_reset;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use id::UserID;
|
use id::UserID;
|
||||||
|
|
@ -51,4 +52,7 @@ impl User {
|
||||||
pub fn updated_at(&self) -> DateTime<Utc> {
|
pub fn updated_at(&self) -> DateTime<Utc> {
|
||||||
self.updated_at
|
self.updated_at
|
||||||
}
|
}
|
||||||
|
pub fn paper_key_hashed(&self) -> Option<&str> {
|
||||||
|
self.paper_key.as_deref()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
crates/users/src/password_reset.rs
Normal file
55
crates/users/src/password_reset.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
use chrono::{DateTime, Duration, TimeZone, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
// error
|
||||||
|
#[derive(thiserror::Error)]
|
||||||
|
// the rest
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Invalid token format")]
|
||||||
|
TokenFormat,
|
||||||
|
|
||||||
|
#[error("Invalid time format")]
|
||||||
|
TimeFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct PasswordResetToken {
|
||||||
|
token: String,
|
||||||
|
expires_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PasswordResetToken {
|
||||||
|
pub fn generate(duration_minutes: i64) -> Self {
|
||||||
|
use gen_passphrase::dictionary::EFF_LARGE;
|
||||||
|
use gen_passphrase::generate;
|
||||||
|
|
||||||
|
let token = generate(&[EFF_LARGE], 10, None);
|
||||||
|
let expires_at = Utc::now() + Duration::minutes(duration_minutes);
|
||||||
|
|
||||||
|
Self { token, expires_at }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(raw: &str) -> Result<Self, Error> {
|
||||||
|
let (token, timestamp_str) = raw.split_once('-').ok_or(Error::TokenFormat)?;
|
||||||
|
let expires_at = Utc
|
||||||
|
.datetime_from_str(timestamp_str, "%s")
|
||||||
|
.map_err(|_| Error::TimeFormat)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
token: token.to_string(),
|
||||||
|
expires_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_expired(&self) -> bool {
|
||||||
|
self.expires_at < Utc::now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PasswordResetToken {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}-{}", self.token, self.expires_at.timestamp())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue