M src/domain/user.rs => src/domain/user.rs +4 -1
@@ 65,7 65,10 @@ where Persistence: RussetUserPersistenceLayer {
}
None => {
// Hash the password anyway to resist user enumeration via side channels
- let parsed_hash = argon2::PasswordHash::new("$argon2id$v=19$m=19456,t=2,p=1$DFhnniX1Kn3JoEKD5e9qbQ$IxgxUYNYPTvPTjez280uFJh166f+eNkCXntlVe5NaZQ").unwrap();
+ let parsed_hash = argon2::PasswordHash::new(
+ "$argon2id$v=19$m=19456,t=2,p=1$DFhnniX1Kn3JoEKD5e9qbQ$\
+ IxgxUYNYPTvPTjez280uFJh166f+eNkCXntlVe5NaZQ"
+ ).expect("hardcoded password hash should parse");
let _ = password_hash.verify_password(&password_bytes, &parsed_hash);
info!("User {:?} not found", user_name);
Ok(None)
M => +3 -1
@@ 23,7 23,9 @@ impl RussetFeedReader for RssFeedReader {
let title = rss.title;
let entries = rss.items.into_iter().map(|item| {
Entry {
internal_id: item.guid.map_or_else(|| item.link.clone().unwrap(), |guid| guid.value().to_string()),
internal_id: item.guid.map_or_else(
|| item.link.clone().expect("TODO: every item might not have either a guid or a link"),
|guid| guid.value().to_string()),
url: item
.link
.map_or(None, |url| Url::parse(&url).ok()),
M src/http/error.rs => src/http/error.rs +9 -3
@@ 1,5 1,5 @@
use axum::http::StatusCode;
-use axum::response::{ Html, IntoResponse, Response };
+use axum::response::{ Html, IntoResponse, Redirect, Response };
use crate::Err;
use crate::persistence::model::User;
use sailfish::RenderError;
@@ 9,7 9,7 @@ use ulid::Ulid;
pub enum HttpError {
BadRequest { description: String },
- Unauthenticated,
+ Unauthenticated { redirect_to: Option<String> },
NotFound,
InternalError { description: String },
}
@@ 41,7 41,13 @@ impl IntoResponse for HttpError {
fn into_response(self) -> Response {
let (status, description) = match self {
HttpError::BadRequest { description } => (StatusCode::BAD_REQUEST, description),
- HttpError::Unauthenticated => (StatusCode::UNAUTHORIZED, "You must <a href=\"/login\">log in</a>.".to_string()),
+ HttpError::Unauthenticated { redirect_to } => {
+ let redirect = format!("/login{}", redirect_to.map_or(
+ "".to_string(),
+ |redirect| format!("?redirect_to={redirect}"),
+ ) );
+ return Redirect::to(&redirect).into_response();
+ },
HttpError::NotFound => (StatusCode::NOT_FOUND, "No resource exists at this URL.".to_string()),
HttpError::InternalError { description } => {
let correlation_id = Ulid::new();
M src/http/login.rs => src/http/login.rs +1 -1
@@ 86,6 86,6 @@ where Persistence: RussetPersistenceLayer {
Redirect::to(&login.redirect_to.unwrap_or("/".to_string())),
))
},
- None => Err(HttpError::Unauthenticated),
+ None => Err(HttpError::Unauthenticated { redirect_to: login.redirect_to }),
}
}
M src/http/mod.rs => src/http/mod.rs +17 -3
@@ 26,8 26,20 @@ pub fn russet_router<Persistence>(
login_concurrent_limit: u32,
) -> Router<AppState<Persistence>>
where Persistence: RussetPersistenceLayer {
- let global_limit_semaphore = Arc::new(Semaphore::new(global_concurrent_limit.try_into().unwrap()));
- let login_limit_sempahore = Arc::new(Semaphore::new(login_concurrent_limit.try_into().unwrap()));
+ let global_limit_semaphore = Arc::new(
+ Semaphore::new(
+ global_concurrent_limit
+ .try_into()
+ .expect("global concurrency limit should fit in a usize")
+ )
+ );
+ let login_limit_sempahore = Arc::new(
+ Semaphore::new(
+ login_concurrent_limit
+ .try_into()
+ .expect("login concurrency limit should *definitely* fit in a usize")
+ )
+ );
Router::new()
.route("/login", post(login::login_user))
.layer(GlobalConcurrencyLimitLayer::with_semaphore(login_limit_sempahore))
@@ 50,7 62,9 @@ async fn csp_header<B>(mut response: Response<B>) -> Response<B> {
.headers_mut()
.insert(
"Content-Security-Policy",
- "script-source none".parse().unwrap(),
+ "script-source none"
+ .parse()
+ .expect("hard-coded header should be encoded correctly"),
);
response
}
M src/http/root.rs => src/http/root.rs +2 -1
@@ 91,7 91,8 @@ impl EditUserEntriesRequest {
},
"select-all" => select_all = true,
key if key.starts_with("select-") => {
- let suffix = key.strip_prefix("select-").unwrap();
+ let suffix = key.strip_prefix("select-")
+ .expect("a string with starts with a given prefix has that prefix");
let id = ulid::Ulid::from_string(suffix)?;
selected_ids.push(EntryId(id));
},
M src/http/session.rs => src/http/session.rs +8 -8
@@ 2,11 2,11 @@ use axum::{ async_trait, RequestPartsExt };
use axum::extract::{ FromRef, FromRequestParts, State };
use axum_extra::extract::cookie::CookieJar;
use axum::http::request::Parts;
-use axum::response::{ IntoResponse, Redirect, Response };
-use core::marker::PhantomData;
use crate::http::AppState;
+use crate::http::error::HttpError;
use crate::persistence::model::User;
use crate::persistence::RussetPersistenceLayer;
+use std::marker::PhantomData;
#[derive(Debug)]
pub struct AuthenticatedUser<Persistence> {
@@ 20,27 20,27 @@ where
Persistence: RussetPersistenceLayer,
AppState<Persistence>: FromRef<S>,
{
- type Rejection = Response;
+ type Rejection = HttpError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let State(state): State<AppState<Persistence>> = State::from_request_parts(parts, state)
.await
- .map_err(|err| err.into_response())?;
+ .expect("Infallible is");
let cookies = parts.extract::<CookieJar>()
.await
- .map_err(|err| err.into_response())?;
+ .expect("Infallible is");
let session_cookie = cookies.get("session_id");
match session_cookie {
Some(session_cookie) => {
- let user = state.domain_service.auth_user(session_cookie.value()).await.unwrap();
+ let user = state.domain_service.auth_user(session_cookie.value()).await?;
match user {
Some(user) => Ok(AuthenticatedUser { user, phantom: PhantomData }),
// Session cookie is present but invalid; user needs to reauthenticate
- None => Err(Redirect::to("/login").into_response()),
+ None => Err(HttpError::Unauthenticated { redirect_to: None }),
}
},
// Session cookies is missing: user needs to authenticate
- None => Err(Redirect::to("/login").into_response()),
+ None => Err(HttpError::Unauthenticated { redirect_to: None }),
}
}
}
M src/http/static_routes.rs => src/http/static_routes.rs +2 -2
@@ 13,6 13,6 @@ pub async fn styles() -> Response<String> {
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/css")
- .body(Css{}.render_once().unwrap())
- .unwrap()
+ .body(Css{}.render_once().expect("rendering a static asset should work"))
+ .expect("building a response with a static asset should work")
}
M src/main.rs => src/main.rs +4 -4
@@ 43,7 43,7 @@ pub type Result<T> = std::result::Result<T, Err>;
#[tokio::main]
async fn main() -> Result<()> {
- init_tracing();
+ init_tracing()?;
// Hierarchy of configs
let config = {
@@ 143,15 143,15 @@ async fn main() -> Result<()> {
Ok(())
}
-fn init_tracing() {
+fn init_tracing() -> Result<()> {
let filter = EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
- .from_env()
- .unwrap();
+ .from_env()?;
let subscriber = tracing_subscriber::fmt::layer();
//` .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE);
tracing_subscriber::registry()
.with(filter)
.with(subscriber)
.init();
+ Ok(())
}
M src/persistence/sql/mod.rs => src/persistence/sql/mod.rs +9 -1
@@ 43,7 43,15 @@ impl TryFrom<Timestamp> for i64 {
type Error = Err;
fn try_from(value: Timestamp) -> Result<i64> {
Ok(value.0.duration_since(SystemTime::UNIX_EPOCH).map_or_else(
- |_| SystemTime::UNIX_EPOCH.duration_since(value.0).unwrap().as_millis().try_into().map(|i: i64| -i),
+ // Err case: value is prior to epoch, so do the comparison in the
+ // other direction and negate the result
+ |_| SystemTime::UNIX_EPOCH
+ .duration_since(value.0)
+ .expect("duration_since should only return Err if the SystemTimes are in reverse order")
+ .as_millis()
+ .try_into()
+ .map(|i: i64| -i),
+ // Ok case: just pull out the milliseconds
|duration| duration.as_millis().try_into(),
)?)
}
M src/persistence/sql/user.rs => src/persistence/sql/user.rs +1 -2
@@ 208,8 208,7 @@ impl RussetUserPersistenceLayer for SqlDatabase {
.execute(&self.pool)
.await?
.rows_affected()
- .try_into()
- .unwrap();
+ .try_into()?;
Ok(rows)
}