From 6e82ae059dc047dbd2b043aa16bef131fe7e2af5 Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Sat, 21 Nov 2020 23:03:56 -0500 Subject: [PATCH] Add user management routes --- examples/user_management.rs | 42 +---- src/user_management/mod.rs | 2 + src/user_management/pages/askcert/exists.gmi | 5 + src/user_management/pages/askcert/success.gmi | 11 ++ src/user_management/pages/login/success.gmi | 7 + src/user_management/pages/login/wrong.gmi | 8 + src/user_management/pages/nsi.gmi | 8 + src/user_management/pages/register/exists.gmi | 6 + .../pages/register/success.gmi | 6 + src/user_management/pages/settings.gmi | 3 + src/user_management/pages/unauth.gmi | 9 ++ src/user_management/routes.rs | 143 ++++++++++++++++++ src/user_management/user.rs | 7 +- 13 files changed, 215 insertions(+), 42 deletions(-) create mode 100644 src/user_management/pages/askcert/exists.gmi create mode 100644 src/user_management/pages/askcert/success.gmi create mode 100644 src/user_management/pages/login/success.gmi create mode 100644 src/user_management/pages/login/wrong.gmi create mode 100644 src/user_management/pages/nsi.gmi create mode 100644 src/user_management/pages/register/exists.gmi create mode 100644 src/user_management/pages/register/success.gmi create mode 100644 src/user_management/pages/settings.gmi create mode 100644 src/user_management/pages/unauth.gmi create mode 100644 src/user_management/routes.rs diff --git a/examples/user_management.rs b/examples/user_management.rs index 8f9d570..0f6c1aa 100644 --- a/examples/user_management.rs +++ b/examples/user_management.rs @@ -1,14 +1,11 @@ use anyhow::*; -use futures_core::future::BoxFuture; -use futures_util::FutureExt; use log::LevelFilter; use northstar::{ - GEMINI_MIME, GEMINI_PORT, Request, Response, Server, - user_management::{User, UserManagerError}, + user_management::UserManagementRoutes, }; #[tokio::main] @@ -19,6 +16,7 @@ async fn main() -> Result<()> { Server::bind(("0.0.0.0", GEMINI_PORT)) .add_route("/", handle_request) + .add_um_routes::("/") .serve() .await } @@ -30,38 +28,6 @@ async fn main() -> Result<()> { /// selecting a username. They'll then get a message confirming their account creation. /// Any time this user visits the site in the future, they'll get a personalized welcome /// message. -fn handle_request(request: Request) -> BoxFuture<'static, Result> { - async move { - Ok(match request.user::()? { - User::Unauthenticated => { - Response::client_certificate_required() - }, - User::NotSignedIn(user) => { - if let Some(username) = request.input() { - match user.register::(username.to_owned()) { - Ok(_user) => Response::success(&GEMINI_MIME) - .with_body("Your account has been created!\n=>/ Begin"), - Err(UserManagerError::UsernameNotUnique) => - Response::input_lossy("That username is taken. Try again"), - Err(e) => panic!("Unexpected error: {}", e), - } - } else { - Response::input_lossy("Please pick a username") - } - }, - User::SignedIn(mut user) => { - if request.path_segments()[0].eq("push") { // User connecting to /push - if let Some(push) = request.input() { - user.as_mut().push_str(push); - user.save()?; - } else { - return Ok(Response::input_lossy("Enter a string to push")); - } - } - - Response::success(&GEMINI_MIME) - .with_body(format!("Your current string: {}\n=> /push Push", user.as_ref())) - } - }) - }.boxed() +async fn handle_request(_request: Request) -> Result { + Ok(Response::success_plain("Base handler")) } diff --git a/src/user_management/mod.rs b/src/user_management/mod.rs index 029f43c..face405 100644 --- a/src/user_management/mod.rs +++ b/src/user_management/mod.rs @@ -20,6 +20,8 @@ //! Use of this module requires the `user_management` feature to be enabled pub mod user; mod manager; +mod routes; +pub use routes::UserManagementRoutes; pub use manager::UserManager; pub use user::User; pub use manager::CertificateData; diff --git a/src/user_management/pages/askcert/exists.gmi b/src/user_management/pages/askcert/exists.gmi new file mode 100644 index 0000000..e516676 --- /dev/null +++ b/src/user_management/pages/askcert/exists.gmi @@ -0,0 +1,5 @@ +Hi {username}! + +It looks like you already have an account all set up, and you're good to go. The link below will take you back to the app. + +=> {redirect} Back diff --git a/src/user_management/pages/askcert/success.gmi b/src/user_management/pages/askcert/success.gmi new file mode 100644 index 0000000..a5f5c85 --- /dev/null +++ b/src/user_management/pages/askcert/success.gmi @@ -0,0 +1,11 @@ +Awesome! + +You're certificate was found, and you're good to go. + +If this is your first time, please create an account to get started! + +=> /account/register Sign Up + +If you already have an account, and this is a new certificate that you'd like to link, you can login using your password (if you've set it) below. + +=> /account/login Log In diff --git a/src/user_management/pages/login/success.gmi b/src/user_management/pages/login/success.gmi new file mode 100644 index 0000000..60b3b9f --- /dev/null +++ b/src/user_management/pages/login/success.gmi @@ -0,0 +1,7 @@ +# Success! + +Welcome {username}! + +Your certificate has been linked. + +=> {redirect} Back to app diff --git a/src/user_management/pages/login/wrong.gmi b/src/user_management/pages/login/wrong.gmi new file mode 100644 index 0000000..72f6425 --- /dev/null +++ b/src/user_management/pages/login/wrong.gmi @@ -0,0 +1,8 @@ +# Wrong username or password + +Sorry {username}, + +It looks like that username and password didn't match. + +=> /account/login/{username} Try another password +=> /account/login That's not me! diff --git a/src/user_management/pages/nsi.gmi b/src/user_management/pages/nsi.gmi new file mode 100644 index 0000000..dadba75 --- /dev/null +++ b/src/user_management/pages/nsi.gmi @@ -0,0 +1,8 @@ +Welcome! + +To continue, please create an account, or log in to link this certificate to an existing account. + +=> /account/login Log In +=> /account/register Sign Up + +Note: You can only link a new certificate if you have set a password using your original certificate. If you haven't already, please log in and set a password. diff --git a/src/user_management/pages/register/exists.gmi b/src/user_management/pages/register/exists.gmi new file mode 100644 index 0000000..bba97b2 --- /dev/null +++ b/src/user_management/pages/register/exists.gmi @@ -0,0 +1,6 @@ +# Username Exists + +Unfortunately, it looks like the username {username} is already taken. If this is your account, and you have a password set, you can link this certificate to your account. Otherwise, please choose another username. + +=> /account/register Choose a different username +=> /account/login/{username} Link this certificate diff --git a/src/user_management/pages/register/success.gmi b/src/user_management/pages/register/success.gmi new file mode 100644 index 0000000..c16ca7c --- /dev/null +++ b/src/user_management/pages/register/success.gmi @@ -0,0 +1,6 @@ +# Account Created! + +Welcome {username}! Your account has been created. The link below will take you back to the app, or you can take a moment to review your account settings, including adding a password. + +=> {redirect} Back +=> /account Settings diff --git a/src/user_management/pages/settings.gmi b/src/user_management/pages/settings.gmi new file mode 100644 index 0000000..df6f4c9 --- /dev/null +++ b/src/user_management/pages/settings.gmi @@ -0,0 +1,3 @@ +Welcome {username}! + +=> {redirect} Back to app diff --git a/src/user_management/pages/unauth.gmi b/src/user_management/pages/unauth.gmi new file mode 100644 index 0000000..8073abf --- /dev/null +++ b/src/user_management/pages/unauth.gmi @@ -0,0 +1,9 @@ +Welcome! + +It seems like you don't have a client certificate enabled. In order to log in, you need to connect using a client certificate. If your client supports it, you can use the link below to activate a certificate. + +=> /account/askcert Choose a Certificate + +If your client can't automatically manage client certificates, check the link below for a list of clients that support client certificates. + +=> /account/clients Clients diff --git a/src/user_management/routes.rs b/src/user_management/routes.rs new file mode 100644 index 0000000..6b46a68 --- /dev/null +++ b/src/user_management/routes.rs @@ -0,0 +1,143 @@ +use anyhow::Result; +use tokio::net::ToSocketAddrs; +use serde::{Serialize, de::DeserializeOwned}; + +use crate::{Request, Response}; +use crate::user_management::{User, RegisteredUser, UserManagerError}; + +const UNAUTH: &str = include_str!("pages/unauth.gmi"); + +pub trait UserManagementRoutes: private::Sealed { + fn add_um_routes(self, redir: &'static str) -> Self; +} + +impl UserManagementRoutes for crate::Builder { + fn add_um_routes(self, redir: &'static str) -> Self { + self.add_route("/account", move|r|handle_base::(r, redir)) + .add_route("/account/askcert", move|r|handle_ask_cert::(r, redir)) + .add_route("/account/register", move|r|handle_register::(r, redir)) + .add_route("/account/login", move|r|handle_login::(r, redir)) + } +} + +async fn handle_base(request: Request, redirect: &'static str) -> Result { + Ok(match request.user::()? { + User::Unauthenticated => { + Response::success_gemini(UNAUTH) + }, + User::NotSignedIn(_) => { + Response::success_gemini(include_str!("pages/nsi.gmi")) + }, + User::SignedIn(user) => { + render_settings_menu(user, redirect) + }, + }) +} + +async fn handle_ask_cert(request: Request, redirect: &'static str) -> Result { + Ok(match request.user::()? { + User::Unauthenticated => { + Response::client_certificate_required() + }, + User::NotSignedIn(_) => { + Response::success_gemini(include_str!("pages/askcert/success.gmi")) + }, + User::SignedIn(user) => { + Response::success_gemini(format!( + include_str!("pages/askcert/exists.gmi"), + username = user.username(), + redirect = redirect, + )) + }, + }) +} + +async fn handle_register(request: Request, redirect: &'static str) -> Result { + Ok(match request.user::()? { + User::Unauthenticated => { + Response::success_gemini(UNAUTH) + }, + User::NotSignedIn(nsi) => { + if let Some(username) = request.input() { + match nsi.register::(username.to_owned()) { + Err(UserManagerError::UsernameNotUnique) => { + Response::success_gemini(format!( + include_str!("pages/register/exists.gmi"), + username = username, + )) + }, + Ok(_) => { + Response::success_gemini(format!( + include_str!("pages/register/success.gmi"), + username = username, + redirect = redirect, + )) + }, + Err(e) => return Err(e.into()) + } + } else { + Response::input_lossy("Please pick a username") + } + }, + User::SignedIn(user) => { + render_settings_menu(user, redirect) + }, + }) +} + +async fn handle_login(request: Request, redirect: &'static str) -> Result { + Ok(match request.user::()? { + User::Unauthenticated => { + Response::success_gemini(UNAUTH) + }, + User::NotSignedIn(nsi) => { + if let Some(username) = request.trailing_segments().get(0) { + if let Some(password) = request.input() { + match nsi.attach::(username, Some(password.as_bytes())) { + Err(UserManagerError::PasswordNotSet) | Ok(None) => { + Response::success_gemini(format!( + include_str!("pages/login/wrong.gmi"), + username = username, + )) + }, + Ok(_) => { + Response::success_gemini(format!( + include_str!("pages/login/success.gmi"), + username = username, + redirect = redirect, + )) + }, + Err(e) => return Err(e.into()), + } + } else { + Response::input_lossy("Please enter your password") + } + } else if let Some(username) = request.input() { + Response::redirect_temporary_lossy( + format!("/account/login/{}", username).as_str() + ) + } else { + Response::input_lossy("Please enter your username") + } + }, + User::SignedIn(user) => { + render_settings_menu(user, redirect) + }, + }) +} + +fn render_settings_menu( + user: RegisteredUser, + redirect: &str +) -> Response { + Response::success_gemini(format!( + include_str!("pages/settings.gmi"), + username = user.username(), + redirect = redirect, + )) +} + +mod private { + pub trait Sealed {} + impl Sealed for crate::Builder {} +} diff --git a/src/user_management/user.rs b/src/user_management/user.rs index 06617c1..702d9ab 100644 --- a/src/user_management/user.rs +++ b/src/user_management/user.rs @@ -155,15 +155,14 @@ impl NotSignedInUser { /// certificate to, consider using [`RegisteredUser::add_certificate()`] /// /// # Errors - /// This will error if the username and password are incorrect, or if the user has yet - /// to set a password. + /// This will error if the user has yet to set a password. /// /// Additional errors might occur if an error occurs during database lookup and /// deserialization pub fn attach( self, - username: impl AsRef, - password: Option>, + username: &str, + password: Option<&[u8]>, ) -> Result>> { if let Some(mut user) = self.manager.lookup_user(username)? { // Perform password check, if caller wants