Skip to content

Commit 3fc79d1

Browse files
committed
feat: unlock with only mnemonic
1 parent d3a1981 commit 3fc79d1

File tree

6 files changed

+139
-89
lines changed

6 files changed

+139
-89
lines changed

crates/rostra-client-db/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ pub enum DbError {
125125
},
126126
#[snafu(visibility(pub))]
127127
#[snafu(display("Provided Id does not match one used previously"))]
128-
IdMismatch {
128+
DbIdMismatch {
129129
#[snafu(implicit)]
130130
location: Location,
131131
},
@@ -536,7 +536,7 @@ impl Database {
536536
pub fn verify_self_tx(self_id: RostraId, ids_self_t: &mut ids_self::Table) -> DbResult<()> {
537537
if let Some(existing_self_id_record) = Self::read_self_id_tx(ids_self_t)? {
538538
if existing_self_id_record.rostra_id != self_id {
539-
return IdMismatchSnafu.fail();
539+
return DbIdMismatchSnafu.fail();
540540
}
541541
} else {
542542
Self::write_self_id_tx(self_id, ids_self_t)?;

crates/rostra-web-ui/src/error.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
use std::io;
2+
13
use axum::http::{HeaderName, HeaderValue, StatusCode};
24
use axum::response::{IntoResponse, Redirect, Response};
3-
use rostra_client::error::PostError;
5+
use rostra_client::error::{ActivateError, InitError, PostError};
6+
use rostra_client::multiclient::MultiClientError;
47
use rostra_client::ClientRefError;
8+
use rostra_client_db::DbError;
59
use rostra_util_error::BoxedError;
610
use serde::Serialize;
711
use snafu::Snafu;
@@ -34,6 +38,33 @@ pub struct UserErrorResponse {
3438
pub message: String,
3539
}
3640

41+
#[derive(Debug, Snafu)]
42+
pub enum UnlockError {
43+
#[snafu(visibility(pub(crate)))]
44+
PublicKeyMissing,
45+
#[snafu(visibility(pub(crate)))]
46+
IdMismatch,
47+
#[snafu(transparent)]
48+
Io {
49+
source: io::Error,
50+
},
51+
Database {
52+
source: DbError,
53+
},
54+
Init {
55+
source: InitError,
56+
},
57+
#[snafu(transparent)]
58+
MultiClient {
59+
source: MultiClientError,
60+
},
61+
#[snafu(transparent)]
62+
MultiClientActivate {
63+
source: ActivateError,
64+
},
65+
}
66+
pub type UnlockResult<T> = std::result::Result<T, UnlockError>;
67+
3768
#[derive(Debug, Snafu)]
3869
pub enum RequestError {
3970
#[snafu(transparent)]
@@ -49,7 +80,9 @@ pub enum RequestError {
4980
#[snafu(visibility(pub(crate)))]
5081
LoginRequired,
5182
#[snafu(visibility(pub(crate)))]
52-
SecretKeyMissing,
83+
Unlock { source: UnlockError },
84+
#[snafu(visibility(pub(crate)))]
85+
ReadOnlyMode,
5386
#[snafu(visibility(pub(crate)))]
5487
User { source: UserRequestError },
5588
}

crates/rostra-web-ui/src/lib.rs

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ mod fragment;
44
pub mod html_utils;
55
pub mod is_htmx;
66
mod routes;
7+
// TODO: move to own crate
8+
mod serde_util;
79

810
use std::net::{AddrParseError, SocketAddr};
911
use std::path::{Path, PathBuf};
@@ -16,13 +18,13 @@ use asset_cache::AssetCache;
1618
use axum::http::header::{ACCEPT, CONTENT_TYPE};
1719
use axum::http::{HeaderName, HeaderValue, Method};
1820
use axum::{middleware, Router};
19-
use rostra_client::error::{ActivateError, IdSecretReadError, InitError};
20-
use rostra_client::multiclient::{MultiClient, MultiClientError};
21+
use error::{IdMismatchSnafu, UnlockError, UnlockResult};
22+
use rostra_client::error::IdSecretReadError;
23+
use rostra_client::multiclient::MultiClient;
2124
use rostra_client::{ClientHandle, ClientRefError};
22-
use rostra_client_db::DbError;
2325
use rostra_core::id::{RostraId, RostraIdSecretKey};
2426
use rostra_util::is_rostra_dev_mode_set;
25-
use rostra_util_error::{BoxedError, WhateverResult};
27+
use rostra_util_error::WhateverResult;
2628
use routes::cache_control;
2729
use snafu::{ensure, ResultExt as _, Snafu, Whatever};
2830
use tokio::net::{TcpListener, TcpSocket};
@@ -89,33 +91,6 @@ pub enum UiStateClientError {
8991
}
9092
pub type UiStateClientResult<T> = result::Result<T, UiStateClientError>;
9193

92-
#[derive(Debug, Snafu)]
93-
pub enum UiStateClientUnlockError {
94-
IdMismatch,
95-
InvalidMnemonic {
96-
source: BoxedError,
97-
},
98-
#[snafu(transparent)]
99-
Io {
100-
source: io::Error,
101-
},
102-
Database {
103-
source: DbError,
104-
},
105-
Init {
106-
source: InitError,
107-
},
108-
#[snafu(transparent)]
109-
MultiClient {
110-
source: MultiClientError,
111-
},
112-
#[snafu(transparent)]
113-
MultiClientActivate {
114-
source: ActivateError,
115-
},
116-
}
117-
pub type UiStateClientUnlockResult<T> = result::Result<T, UiStateClientUnlockError>;
118-
11994
pub struct UiState {
12095
clients: MultiClient,
12196
}
@@ -132,21 +107,17 @@ impl UiState {
132107
pub async fn unlock(
133108
&self,
134109
rostra_id: RostraId,
135-
mnemonic: &str,
136-
) -> UiStateClientUnlockResult<Option<RostraIdSecretKey>> {
137-
let res = if mnemonic.trim().is_empty() {
138-
self.clients.load(rostra_id).await?;
139-
None
140-
} else {
141-
let secret_id = RostraIdSecretKey::from_str(mnemonic)
142-
.boxed()
143-
.context(InvalidMnemonicSnafu)?;
144-
110+
secret_id: Option<RostraIdSecretKey>,
111+
) -> UnlockResult<Option<RostraIdSecretKey>> {
112+
let res = if let Some(secret_id) = secret_id {
145113
ensure!(secret_id.id() == rostra_id, IdMismatchSnafu);
146-
let client = self.clients.load(rostra_id).await?;
114+
let client = self.clients.load(secret_id.id()).await?;
147115
client.unlock_active(secret_id).await?;
148116

149117
Some(secret_id)
118+
} else {
119+
self.clients.load(rostra_id).await?;
120+
None
150121
};
151122
Ok(res)
152123
}
@@ -173,7 +144,7 @@ pub enum WebUiServerError {
173144
},
174145

175146
SecretUnlock {
176-
source: UiStateClientUnlockError,
147+
source: UnlockError,
177148
},
178149

179150
ListenAddr {

crates/rostra-web-ui/src/routes/unlock.rs

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ use snafu::ResultExt as _;
1212
use tower_sessions::Session;
1313

1414
use super::Maud;
15-
use crate::error::{OtherSnafu, RequestResult};
15+
use crate::error::{
16+
LoginRequiredSnafu, OtherSnafu, PublicKeyMissingSnafu, RequestResult, UnlockResult, UnlockSnafu,
17+
};
1618
use crate::is_htmx::IsHtmx;
19+
use crate::serde_util::empty_string_as_none;
1720
use crate::{SharedState, UiState};
1821

1922
pub async fn get(
@@ -31,64 +34,76 @@ pub async fn get(
3134
)];
3235
return Ok((StatusCode::OK, headers).into_response());
3336
}
34-
Ok(Maud(state.unlock_page(None, "", None).await?).into_response())
37+
Ok(Maud(state.unlock_page(None, None, None).await?).into_response())
3538
}
3639

3740
pub async fn get_random(state: State<SharedState>) -> RequestResult<impl IntoResponse> {
3841
let random_secret_key = RostraIdSecretKey::generate();
3942
Ok(Maud(
4043
state
41-
.unlock_page(
42-
Some(random_secret_key.id()),
43-
&random_secret_key.to_string(),
44-
None,
45-
)
44+
.unlock_page(Some(random_secret_key.id()), Some(random_secret_key), None)
4645
.await?,
4746
))
4847
}
4948

5049
#[derive(Deserialize)]
5150
pub struct Input {
5251
#[serde(rename = "username")]
53-
rostra_id: RostraId,
52+
#[serde(deserialize_with = "empty_string_as_none")]
53+
rostra_id: Option<RostraId>,
5454
#[serde(rename = "password")]
55-
mnemonic: String,
55+
#[serde(deserialize_with = "empty_string_as_none")]
56+
mnemonic: Option<RostraIdSecretKey>,
57+
}
58+
59+
impl Input {
60+
fn rostra_id(&self) -> UnlockResult<RostraId> {
61+
self.rostra_id
62+
.or_else(|| self.mnemonic.map(|m| m.id()))
63+
.ok_or_else(|| PublicKeyMissingSnafu.build())
64+
}
5665
}
5766

5867
pub async fn post_unlock(
5968
state: State<SharedState>,
6069
session: Session,
6170
Form(form): Form<Input>,
6271
) -> RequestResult<Response> {
63-
Ok(match state.unlock(form.rostra_id, &form.mnemonic).await {
64-
Ok(secret_key_opt) => {
65-
session
66-
.insert(
67-
SESSION_KEY,
68-
&UserSession::new(form.rostra_id, secret_key_opt),
69-
)
70-
.await
71-
.boxed()
72-
.context(OtherSnafu)?;
73-
let headers = [(
74-
HeaderName::from_static("hx-redirect"),
75-
HeaderValue::from_static("/ui"),
76-
)];
77-
(StatusCode::SEE_OTHER, headers).into_response()
78-
}
79-
Err(e) => Maud(
80-
state
81-
.unlock_page(
82-
Some(form.rostra_id),
83-
&form.mnemonic,
84-
html! {
85-
span ."o-unlockScreen_notice" { (e)}
86-
},
87-
)
88-
.await?,
89-
)
90-
.into_response(),
91-
})
72+
Ok(
73+
match state
74+
.unlock(form.rostra_id().context(UnlockSnafu)?, form.mnemonic)
75+
.await
76+
{
77+
Ok(secret_key_opt) => {
78+
let rostra_id = secret_key_opt
79+
.map(|secret| secret.id())
80+
.or(form.rostra_id)
81+
.ok_or_else(|| LoginRequiredSnafu.build())?;
82+
session
83+
.insert(SESSION_KEY, &UserSession::new(rostra_id, secret_key_opt))
84+
.await
85+
.boxed()
86+
.context(OtherSnafu)?;
87+
let headers = [(
88+
HeaderName::from_static("hx-redirect"),
89+
HeaderValue::from_static("/ui"),
90+
)];
91+
(StatusCode::SEE_OTHER, headers).into_response()
92+
}
93+
Err(e) => Maud(
94+
state
95+
.unlock_page(
96+
form.rostra_id,
97+
form.mnemonic,
98+
html! {
99+
span ."o-unlockScreen_notice" { (e)}
100+
},
101+
)
102+
.await?,
103+
)
104+
.into_response(),
105+
},
106+
)
92107
}
93108

94109
pub async fn logout(session: Session) -> RequestResult<impl IntoResponse> {
@@ -105,7 +120,7 @@ impl UiState {
105120
async fn unlock_page(
106121
&self,
107122
current_rostra_id: Option<RostraId>,
108-
current_mnemonic: &str,
123+
current_secret_key: Option<RostraIdSecretKey>,
109124
notification: impl Into<Option<Markup>>,
110125
) -> RequestResult<Markup> {
111126
let random_rostra_id_secret = &RostraIdSecretKey::generate();
@@ -150,7 +165,7 @@ impl UiState {
150165
autocomplete="current-password"
151166
placeholder="Mnemonic (Secret Key) - if empty, read-only mode"
152167
title="Mnemonic is 12 words passphrase encoding secret key of your identity"
153-
value=(current_mnemonic)
168+
value=(current_secret_key.as_ref().map(ToString::to_string).unwrap_or_default())
154169
{ }
155170
button
156171
type="button" // do not submit the form!

crates/rostra-web-ui/src/routes/unlock/session.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
55
use tower_sessions::Session;
66

77
use crate::error::{
8-
InternalServerSnafu, LoginRequiredSnafu, RequestError, RequestResult, SecretKeyMissingSnafu,
8+
InternalServerSnafu, LoginRequiredSnafu, ReadOnlyModeSnafu, RequestError, RequestResult,
99
};
1010

1111
#[derive(Clone, Deserialize, Serialize)]
@@ -20,7 +20,7 @@ impl UserSession {
2020
}
2121

2222
pub(crate) fn id_secret(&self) -> RequestResult<RostraIdSecretKey> {
23-
self.id_secret.ok_or_else(|| SecretKeyMissingSnafu.build())
23+
self.id_secret.ok_or_else(|| ReadOnlyModeSnafu.build())
2424
}
2525

2626
pub(crate) fn new(rostra_id: RostraId, secret_key: Option<RostraIdSecretKey>) -> Self {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use serde::de::IntoDeserializer as _;
2+
use serde::{Deserialize, Deserializer};
3+
4+
pub(crate) fn empty_string_as_none<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
5+
where
6+
D: Deserializer<'de>,
7+
T: Deserialize<'de>,
8+
{
9+
if let Some(str) = Option::<String>::deserialize(deserializer)? {
10+
let str = str.trim();
11+
if str.is_empty() {
12+
Ok(None)
13+
} else {
14+
T::deserialize(str.into_deserializer()).map(Some)
15+
}
16+
} else {
17+
Ok(None)
18+
}
19+
}
20+
21+
#[allow(dead_code)]
22+
pub(crate) fn trim_string<'de, D, T>(deserializer: D) -> Result<T, D::Error>
23+
where
24+
D: Deserializer<'de>,
25+
T: Deserialize<'de>,
26+
{
27+
let s = String::deserialize(deserializer)?;
28+
let s = s.trim();
29+
30+
T::deserialize(s.into_deserializer())
31+
}

0 commit comments

Comments
 (0)