Add user management routes
This commit is contained in:
parent
a9b347a8c9
commit
6e82ae059d
|
@ -1,14 +1,11 @@
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use futures_core::future::BoxFuture;
|
|
||||||
use futures_util::FutureExt;
|
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use northstar::{
|
use northstar::{
|
||||||
GEMINI_MIME,
|
|
||||||
GEMINI_PORT,
|
GEMINI_PORT,
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
Server,
|
Server,
|
||||||
user_management::{User, UserManagerError},
|
user_management::UserManagementRoutes,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -19,6 +16,7 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
Server::bind(("0.0.0.0", GEMINI_PORT))
|
Server::bind(("0.0.0.0", GEMINI_PORT))
|
||||||
.add_route("/", handle_request)
|
.add_route("/", handle_request)
|
||||||
|
.add_um_routes::<String>("/")
|
||||||
.serve()
|
.serve()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -30,38 +28,6 @@ async fn main() -> Result<()> {
|
||||||
/// selecting a username. They'll then get a message confirming their account creation.
|
/// 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
|
/// Any time this user visits the site in the future, they'll get a personalized welcome
|
||||||
/// message.
|
/// message.
|
||||||
fn handle_request(request: Request) -> BoxFuture<'static, Result<Response>> {
|
async fn handle_request(_request: Request) -> Result<Response> {
|
||||||
async move {
|
Ok(Response::success_plain("Base handler"))
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
//! Use of this module requires the `user_management` feature to be enabled
|
//! Use of this module requires the `user_management` feature to be enabled
|
||||||
pub mod user;
|
pub mod user;
|
||||||
mod manager;
|
mod manager;
|
||||||
|
mod routes;
|
||||||
|
pub use routes::UserManagementRoutes;
|
||||||
pub use manager::UserManager;
|
pub use manager::UserManager;
|
||||||
pub use user::User;
|
pub use user::User;
|
||||||
pub use manager::CertificateData;
|
pub use manager::CertificateData;
|
||||||
|
|
5
src/user_management/pages/askcert/exists.gmi
Normal file
5
src/user_management/pages/askcert/exists.gmi
Normal 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
|
11
src/user_management/pages/askcert/success.gmi
Normal file
11
src/user_management/pages/askcert/success.gmi
Normal 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
|
7
src/user_management/pages/login/success.gmi
Normal file
7
src/user_management/pages/login/success.gmi
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Success!
|
||||||
|
|
||||||
|
Welcome {username}!
|
||||||
|
|
||||||
|
Your certificate has been linked.
|
||||||
|
|
||||||
|
=> {redirect} Back to app
|
8
src/user_management/pages/login/wrong.gmi
Normal file
8
src/user_management/pages/login/wrong.gmi
Normal 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!
|
8
src/user_management/pages/nsi.gmi
Normal file
8
src/user_management/pages/nsi.gmi
Normal 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.
|
6
src/user_management/pages/register/exists.gmi
Normal file
6
src/user_management/pages/register/exists.gmi
Normal 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
|
6
src/user_management/pages/register/success.gmi
Normal file
6
src/user_management/pages/register/success.gmi
Normal 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
|
3
src/user_management/pages/settings.gmi
Normal file
3
src/user_management/pages/settings.gmi
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
Welcome {username}!
|
||||||
|
|
||||||
|
=> {redirect} Back to app
|
9
src/user_management/pages/unauth.gmi
Normal file
9
src/user_management/pages/unauth.gmi
Normal 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
|
143
src/user_management/routes.rs
Normal file
143
src/user_management/routes.rs
Normal 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> {}
|
||||||
|
}
|
|
@ -155,15 +155,14 @@ impl NotSignedInUser {
|
||||||
/// certificate to, consider using [`RegisteredUser::add_certificate()`]
|
/// certificate to, consider using [`RegisteredUser::add_certificate()`]
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// This will error if the username and password are incorrect, or if the user has yet
|
/// This will error if the user has yet to set a password.
|
||||||
/// to set a password.
|
|
||||||
///
|
///
|
||||||
/// Additional errors might occur if an error occurs during database lookup and
|
/// Additional errors might occur if an error occurs during database lookup and
|
||||||
/// deserialization
|
/// deserialization
|
||||||
pub fn attach<UserData: Serialize + DeserializeOwned>(
|
pub fn attach<UserData: Serialize + DeserializeOwned>(
|
||||||
self,
|
self,
|
||||||
username: impl AsRef<str>,
|
username: &str,
|
||||||
password: Option<impl AsRef<[u8]>>,
|
password: Option<&[u8]>,
|
||||||
) -> Result<Option<RegisteredUser<UserData>>> {
|
) -> Result<Option<RegisteredUser<UserData>>> {
|
||||||
if let Some(mut user) = self.manager.lookup_user(username)? {
|
if let Some(mut user) = self.manager.lookup_user(username)? {
|
||||||
// Perform password check, if caller wants
|
// Perform password check, if caller wants
|
||||||
|
|
Loading…
Reference in a new issue