Add user management routes

This commit is contained in:
Emi Tatsuo 2020-11-21 23:03:56 -05:00
parent a9b347a8c9
commit 6e82ae059d
Signed by: Emi
GPG key ID: 68FAB2E2E6DFC98B
13 changed files with 215 additions and 42 deletions

View file

@ -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::<String>("/")
.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<Response>> {
async move {
Ok(match request.user::<String>()? {
User::Unauthenticated => {
Response::client_certificate_required()
},
User::NotSignedIn(user) => {
if let Some(username) = request.input() {
match user.register::<String>(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<Response> {
Ok(Response::success_plain("Base handler"))
}

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,7 @@
# Success!
Welcome {username}!
Your certificate has been linked.
=> {redirect} Back to app

View file

@ -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!

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,3 @@
Welcome {username}!
=> {redirect} Back to app

View file

@ -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

View file

@ -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<UserData: Serialize + DeserializeOwned + Default + 'static>(self, redir: &'static str) -> Self;
}
impl<A: ToSocketAddrs> UserManagementRoutes for crate::Builder<A> {
fn add_um_routes<UserData: Serialize + DeserializeOwned + Default + 'static>(self, redir: &'static str) -> Self {
self.add_route("/account", move|r|handle_base::<UserData>(r, redir))
.add_route("/account/askcert", move|r|handle_ask_cert::<UserData>(r, redir))
.add_route("/account/register", move|r|handle_register::<UserData>(r, redir))
.add_route("/account/login", move|r|handle_login::<UserData>(r, redir))
}
}
async fn handle_base<UserData: Serialize + DeserializeOwned>(request: Request, redirect: &'static str) -> Result<Response> {
Ok(match request.user::<UserData>()? {
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<UserData: Serialize + DeserializeOwned>(request: Request, redirect: &'static str) -> Result<Response> {
Ok(match request.user::<UserData>()? {
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<UserData: Serialize + DeserializeOwned + Default>(request: Request, redirect: &'static str) -> Result<Response> {
Ok(match request.user::<UserData>()? {
User::Unauthenticated => {
Response::success_gemini(UNAUTH)
},
User::NotSignedIn(nsi) => {
if let Some(username) = request.input() {
match nsi.register::<UserData>(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<UserData: Serialize + DeserializeOwned + Default>(request: Request, redirect: &'static str) -> Result<Response> {
Ok(match request.user::<UserData>()? {
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::<UserData>(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<UserData: Serialize + DeserializeOwned>(
user: RegisteredUser<UserData>,
redirect: &str
) -> Response {
Response::success_gemini(format!(
include_str!("pages/settings.gmi"),
username = user.username(),
redirect = redirect,
))
}
mod private {
pub trait Sealed {}
impl<A> Sealed for crate::Builder<A> {}
}

View file

@ -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<UserData: Serialize + DeserializeOwned>(
self,
username: impl AsRef<str>,
password: Option<impl AsRef<[u8]>>,
username: &str,
password: Option<&[u8]>,
) -> Result<Option<RegisteredUser<UserData>>> {
if let Some(mut user) = self.manager.lookup_user(username)? {
// Perform password check, if caller wants