530 lines
17 KiB
Rust
530 lines
17 KiB
Rust
use chrono::Duration;
|
|
use cookie::{Cookie, SameSite};
|
|
use failure::Fail;
|
|
use iron::headers::{Cookie as CookieHdr, SetCookie};
|
|
use iron::prelude::*;
|
|
use iron::status;
|
|
use iron::typemap::Key;
|
|
use iron::mime::Mime;
|
|
use iron::modifiers::Redirect;
|
|
use iron::Url;
|
|
use iron::{typemap, BeforeMiddleware,AfterMiddleware};
|
|
use urlencoded::UrlEncodedQuery;
|
|
use persistent::{Read, Write};
|
|
use router::Router;
|
|
use mount::{Mount};
|
|
use serde::{Serialize, Deserialize};
|
|
use lettre::{SendableEmail, SmtpClient, SmtpTransport, Transport};
|
|
|
|
use acp;
|
|
use account;
|
|
use mail;
|
|
use mail::Mail;
|
|
use pg::PgPool;
|
|
use payments::{stripe};
|
|
|
|
pub const TOKEN_HEADER: &str = "x-auth-token";
|
|
pub const AUTH_CLEAR: &str =
|
|
"x-auth-token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=-1;";
|
|
|
|
#[derive(Clone, Copy, Fail, Debug, Serialize, Deserialize)]
|
|
pub enum MnmlHttpError {
|
|
// User Facing Errors
|
|
#[fail(display="internal server error")]
|
|
ServerError,
|
|
#[fail(display="internal server error")]
|
|
DbError,
|
|
#[fail(display="unauthorized")]
|
|
Unauthorized,
|
|
#[fail(display="bad request")]
|
|
BadRequest,
|
|
#[fail(display="not found")]
|
|
NotFound,
|
|
#[fail(display="account name not provided")]
|
|
AccountNameNotProvided,
|
|
#[fail(display="account name unavailable")]
|
|
AccountNameUnavailable,
|
|
#[fail(display="account name is unacceptable. 20 char max")]
|
|
AccountNameUnacceptable,
|
|
#[fail(display="account not found")]
|
|
AccountNotFound,
|
|
#[fail(display="password does not match")]
|
|
PasswordNotMatch,
|
|
#[fail(display="password unacceptable. must be > 11 characters")]
|
|
PasswordUnacceptable,
|
|
#[fail(display="incorrect token. refresh or logout of existing sessions")]
|
|
TokenDoesNotMatch,
|
|
}
|
|
|
|
impl From<bcrypt::BcryptError> for MnmlHttpError {
|
|
fn from(err: bcrypt::BcryptError) -> Self {
|
|
warn!("{:?}", err);
|
|
MnmlHttpError::ServerError
|
|
}
|
|
}
|
|
|
|
impl From<postgres::Error> for MnmlHttpError {
|
|
fn from(err: postgres::Error) -> Self {
|
|
warn!("{:?}", err);
|
|
|
|
match err.as_db() {
|
|
Some(db) => {
|
|
let constraint = match db.constraint {
|
|
Some(ref c) => c,
|
|
None => return MnmlHttpError::DbError,
|
|
};
|
|
|
|
match constraint.as_ref() {
|
|
"accounts_name_unique" => MnmlHttpError::AccountNameUnavailable,
|
|
_ => MnmlHttpError::DbError,
|
|
}
|
|
},
|
|
_ => MnmlHttpError::DbError,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<r2d2::Error> for MnmlHttpError {
|
|
fn from(err: r2d2::Error) -> Self {
|
|
warn!("{:?}", err);
|
|
MnmlHttpError::DbError
|
|
}
|
|
}
|
|
|
|
impl From<failure::Error> for MnmlHttpError {
|
|
fn from(err: failure::Error) -> Self {
|
|
warn!("{:?}", err);
|
|
MnmlHttpError::ServerError
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
#[serde(rename_all(serialize = "lowercase"))]
|
|
pub enum Json {
|
|
Error(String),
|
|
Message(String),
|
|
}
|
|
|
|
pub fn json_response(status: status::Status, response: Json) -> Response {
|
|
let content_type = "application/json".parse::<Mime>().unwrap();
|
|
let json = serde_json::to_string(&response).unwrap();
|
|
return Response::with((content_type, status, json));
|
|
}
|
|
|
|
pub fn json_object(status: status::Status, object: String) -> Response {
|
|
let content_type = "application/json".parse::<Mime>().unwrap();
|
|
return Response::with((content_type, status, object));
|
|
}
|
|
|
|
impl From<MnmlHttpError> for IronError {
|
|
fn from(m_err: MnmlHttpError) -> Self {
|
|
let (err, status) = match m_err {
|
|
MnmlHttpError::ServerError |
|
|
MnmlHttpError::DbError => (m_err.compat(), status::InternalServerError),
|
|
|
|
MnmlHttpError::AccountNameNotProvided |
|
|
MnmlHttpError::AccountNameUnavailable |
|
|
MnmlHttpError::AccountNameUnacceptable |
|
|
MnmlHttpError::AccountNotFound |
|
|
MnmlHttpError::BadRequest |
|
|
MnmlHttpError::PasswordUnacceptable => (m_err.compat(), status::BadRequest),
|
|
|
|
MnmlHttpError::PasswordNotMatch |
|
|
MnmlHttpError::TokenDoesNotMatch |
|
|
MnmlHttpError::Unauthorized => (m_err.compat(), status::Unauthorized),
|
|
|
|
MnmlHttpError::NotFound => (m_err.compat(), status::NotFound),
|
|
};
|
|
|
|
let response = json_response(status, Json::Error(m_err.to_string()));
|
|
IronError { error: Box::new(err), response }
|
|
}
|
|
}
|
|
|
|
impl typemap::Key for account::Account { type Value = account::Account; }
|
|
struct AuthMiddleware;
|
|
impl BeforeMiddleware for AuthMiddleware {
|
|
fn before(&self, req: &mut Request) -> IronResult<()> {
|
|
let state = req.get::<Read<State>>().unwrap();
|
|
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
|
|
|
match req.headers.get::<CookieHdr>() {
|
|
Some(cookies) => {
|
|
for c in cookies.iter() {
|
|
let cookie = Cookie::parse(c)
|
|
.or(Err(MnmlHttpError::BadRequest))?;
|
|
|
|
// got auth token
|
|
if cookie.name() == TOKEN_HEADER {
|
|
match account::from_token(&db, &cookie.value().to_string()) {
|
|
Ok(a) => req.extensions.insert::<account::Account>(a),
|
|
Err(_) => return Err(MnmlHttpError::TokenDoesNotMatch.into()),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
None => (),
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
struct ErrorHandler;
|
|
impl AfterMiddleware for ErrorHandler {
|
|
fn catch(&self, _: &mut Request, mut err: IronError) -> IronResult<Response> {
|
|
|
|
// on unauthorized we clear the auth token
|
|
match err.response.status {
|
|
Some(status::Unauthorized) => {
|
|
err.response.headers.set(SetCookie(vec![AUTH_CLEAR.to_string()]));
|
|
},
|
|
_ => (),
|
|
};
|
|
|
|
warn!("{:?}", err);
|
|
|
|
return Err(err);
|
|
}
|
|
}
|
|
|
|
fn token_res(token: String) -> Response {
|
|
let v = Cookie::build(TOKEN_HEADER, token)
|
|
.http_only(true)
|
|
.same_site(SameSite::Strict)
|
|
.path("/")
|
|
.max_age(Duration::weeks(1)) // 1 week aligns with db set
|
|
.finish();
|
|
|
|
let mut res = json_response(
|
|
status::Ok,
|
|
Json::Message("authenticated".to_string())
|
|
);
|
|
|
|
res.headers.set(SetCookie(vec![v.to_string()]));
|
|
|
|
return res;
|
|
}
|
|
|
|
|
|
#[derive(Debug,Clone,Deserialize)]
|
|
struct RegisterBody {
|
|
name: String,
|
|
password: String,
|
|
}
|
|
|
|
fn register(req: &mut Request) -> IronResult<Response> {
|
|
let state = req.get::<Read<State>>().unwrap();
|
|
let params = match req.get::<bodyparser::Struct<RegisterBody>>() {
|
|
Ok(Some(b)) => b,
|
|
_ => return Err(MnmlHttpError::BadRequest.into()),
|
|
};
|
|
|
|
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
|
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
|
|
|
|
match account::create(¶ms.name, ¶ms.password, &mut tx) {
|
|
Ok(token) => {
|
|
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
|
Ok(token_res(token))
|
|
},
|
|
Err(e) => {
|
|
warn!("{:?}", e);
|
|
Err(e.into())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug,Clone,Deserialize)]
|
|
struct LoginBody {
|
|
name: String,
|
|
password: String,
|
|
}
|
|
|
|
fn login(req: &mut Request) -> IronResult<Response> {
|
|
let state = req.get::<Read<State>>().unwrap();
|
|
let params = match req.get::<bodyparser::Struct<LoginBody>>() {
|
|
Ok(Some(b)) => b,
|
|
_ => return Err(MnmlHttpError::BadRequest.into()),
|
|
};
|
|
|
|
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
|
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
|
|
|
|
match account::login(&mut tx, ¶ms.name, ¶ms.password) {
|
|
Ok(a) => {
|
|
let token = account::new_token(&mut tx, a.id)?;
|
|
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
|
Ok(token_res(token))
|
|
},
|
|
Err(e) => {
|
|
warn!("{:?}", e);
|
|
Err(e.into())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn logout(req: &mut Request) -> IronResult<Response> {
|
|
let state = req.get::<Read<State>>().unwrap();
|
|
match req.extensions.get::<account::Account>() {
|
|
Some(a) => {
|
|
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
|
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
|
|
|
|
account::new_token(&mut tx, a.id)?;
|
|
|
|
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
|
|
|
let mut res = json_response(status::Ok, Json::Message("logged out".to_string()));
|
|
res.headers.set(SetCookie(vec![AUTH_CLEAR.to_string()]));
|
|
Ok(res)
|
|
},
|
|
None => Err(MnmlHttpError::Unauthorized.into()),
|
|
}
|
|
}
|
|
|
|
fn recover_set(req: &mut Request) -> IronResult<Response> {
|
|
let state = req.get::<Read<State>>().unwrap();
|
|
let params = match req.get::<bodyparser::Struct<EmailPost>>() {
|
|
Ok(Some(b)) => b,
|
|
_ => return Err(MnmlHttpError::BadRequest.into()),
|
|
};
|
|
|
|
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
|
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
|
|
|
|
let user_email = match mail::select(&db, ¶ms.email) {
|
|
Ok(e) => match e.confirmed {
|
|
true => e,
|
|
false => return Ok(json_response(status::NotFound,
|
|
Json::Error("your email is not confirmed.\nplease contact support at humans@mnml.gg".to_string()))),
|
|
},
|
|
Err(_e) => return Ok(json_response(status::NotFound,
|
|
Json::Error("email not registered.\nplease contact support at humans@mnml.gg".to_string()))),
|
|
};
|
|
|
|
let account = account::select(&db, user_email.account)
|
|
.or(Err(MnmlHttpError::NotFound))?;
|
|
|
|
let token = mail::set_recovery(&mut tx, &user_email.email)
|
|
.or(Err(MnmlHttpError::ServerError))?;
|
|
|
|
let app_mailer = req.get::<Write<Mailer>>().unwrap();
|
|
let mut lock = app_mailer.lock().unwrap();
|
|
let message = Mail::Recover { email: user_email.email.clone(), name: account.name.clone(), token };
|
|
|
|
let send = match mail::send_mail(&mut lock.mailer, message) {
|
|
Ok(send) => send,
|
|
Err(e) => {
|
|
warn!("{:?}", e);
|
|
return Err(MnmlHttpError::ServerError.into());
|
|
}
|
|
};
|
|
|
|
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
|
|
|
info!("recovery email sent send={:?} account={:?} email={:?}", send, account, user_email.email);
|
|
Ok(json_response(status::Ok, Json::Message("recovery email sent. check your mailbox for access".to_string())))
|
|
}
|
|
|
|
fn recover(req: &mut Request) -> IronResult<Response> {
|
|
let state = req.get::<Read<State>>().unwrap();
|
|
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
|
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
|
|
let token = match req.get_ref::<UrlEncodedQuery>() {
|
|
Ok(ref hashmap) => {
|
|
match hashmap.get("recover_token") {
|
|
Some(t) => &t[0],
|
|
None => return Err(MnmlHttpError::BadRequest.into()),
|
|
}
|
|
},
|
|
Err(_) => return Err(MnmlHttpError::BadRequest.into()),
|
|
};
|
|
|
|
let user_email = match mail::get_recovery(&mut tx, &token.to_string()) {
|
|
Ok(a) => a,
|
|
Err(_) => return Err(MnmlHttpError::Unauthorized.into()),
|
|
};
|
|
|
|
let token = account::new_token(&mut tx, user_email.account)
|
|
.or(Err(MnmlHttpError::ServerError))?;
|
|
|
|
let account = account::from_token(&db, &token)
|
|
.or(Err(MnmlHttpError::ServerError))?;
|
|
|
|
let v = Cookie::build(TOKEN_HEADER, token)
|
|
.http_only(true)
|
|
.same_site(SameSite::Strict)
|
|
.path("/")
|
|
.max_age(Duration::weeks(1)) // 1 week aligns with db set
|
|
.finish();
|
|
|
|
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
|
let mut res = Response::with((status::SeeOther, Redirect(Url::parse("https://mnml.gg").unwrap())));
|
|
res.headers.set(SetCookie(vec![v.to_string()]));
|
|
|
|
info!("recovered account account={:?}", account);
|
|
Ok(res)
|
|
}
|
|
|
|
#[derive(Debug,Clone,Deserialize)]
|
|
struct SetPassword {
|
|
current: String,
|
|
password: String,
|
|
}
|
|
|
|
fn set_password(req: &mut Request) -> IronResult<Response> {
|
|
let state = req.get::<Read<State>>().unwrap();
|
|
let params = match req.get::<bodyparser::Struct<SetPassword>>() {
|
|
Ok(Some(b)) => b,
|
|
_ => return Err(MnmlHttpError::BadRequest.into()),
|
|
};
|
|
|
|
match req.extensions.get::<account::Account>() {
|
|
Some(a) => {
|
|
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
|
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
|
|
|
|
let token = account::set_password(&mut tx, a.id, ¶ms.current, ¶ms.password)?;
|
|
|
|
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
|
|
|
Ok(token_res(token))
|
|
},
|
|
None => Err(MnmlHttpError::Unauthorized.into()),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug,Clone,Deserialize)]
|
|
struct EmailPost {
|
|
email: String,
|
|
}
|
|
|
|
fn email_set(req: &mut Request) -> IronResult<Response> {
|
|
let state = req.get::<Read<State>>().unwrap();
|
|
let params = match req.get::<bodyparser::Struct<EmailPost>>() {
|
|
Ok(Some(b)) => b,
|
|
_ => return Err(MnmlHttpError::BadRequest.into()),
|
|
};
|
|
|
|
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
|
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
|
|
|
|
let (email, account, token) = match req.extensions.get::<account::Account>() {
|
|
Some(a) => {
|
|
let (_id, token) = match mail::set(&mut tx, a.id, ¶ms.email) {
|
|
Ok(res) => res,
|
|
Err(e) => {
|
|
warn!("{:?}", e);
|
|
return Err(MnmlHttpError::ServerError.into());
|
|
},
|
|
};
|
|
|
|
(params.email.clone(), a.clone(), token)
|
|
},
|
|
None => return Err(MnmlHttpError::Unauthorized.into()),
|
|
};
|
|
|
|
let app_mailer = req.get::<Write<Mailer>>().unwrap();
|
|
let mut lock = app_mailer.lock().unwrap();
|
|
let message = Mail::Confirm { email: email.clone(), name: account.name.clone(), token };
|
|
|
|
let send = match mail::send_mail(&mut lock.mailer, message) {
|
|
Ok(send) => send,
|
|
Err(e) => {
|
|
warn!("{:?}", e);
|
|
return Err(MnmlHttpError::ServerError.into());
|
|
}
|
|
};
|
|
|
|
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
|
|
|
info!("confirmation email sent send={:?} account={:?} email={:?}", send, account, email);
|
|
Ok(json_response(status::Ok, Json::Message("email set. confirmation required".to_string())))
|
|
}
|
|
|
|
fn email_confirm(req: &mut Request) -> IronResult<Response> {
|
|
let state = req.get::<Read<State>>().unwrap();
|
|
|
|
let account = match req.extensions.get::<account::Account>() {
|
|
Some(a) => a.clone(),
|
|
None => return Err(MnmlHttpError::Unauthorized.into()),
|
|
};
|
|
|
|
match req.get_ref::<UrlEncodedQuery>() {
|
|
Ok(ref hashmap) => {
|
|
let db = state.pool.get().or(Err(MnmlHttpError::DbError))?;
|
|
let mut tx = db.transaction().or(Err(MnmlHttpError::DbError))?;
|
|
|
|
let token = match hashmap.get("confirm_token") {
|
|
Some(t) => &t[0],
|
|
None => return Err(MnmlHttpError::BadRequest.into()),
|
|
};
|
|
|
|
let confirmation = match mail::confirm_email(&mut tx, &account, token.to_string()) {
|
|
Ok(c) => c,
|
|
Err(_) => return Err(MnmlHttpError::NotFound.into())
|
|
};
|
|
|
|
info!("email confirmed email={:?} account={:?}", confirmation.0, account);
|
|
|
|
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
|
Ok(Response::with((status::Found, Redirect(Url::parse("https://mnml.gg").unwrap()))))
|
|
},
|
|
Err(_) => Err(MnmlHttpError::BadRequest.into()),
|
|
}
|
|
}
|
|
|
|
const MAX_BODY_LENGTH: usize = 1024 * 1024 * 10;
|
|
|
|
pub struct State {
|
|
pub pool: PgPool,
|
|
}
|
|
|
|
pub struct Mailer {
|
|
pub mailer: SmtpTransport,
|
|
}
|
|
|
|
impl Key for State { type Value = State; }
|
|
impl Key for Mailer { type Value = Mailer; }
|
|
|
|
fn account_mount() -> Router {
|
|
let mut router = Router::new();
|
|
|
|
router.post("login", login, "login");
|
|
router.post("logout", logout, "logout");
|
|
router.post("register", register, "register");
|
|
router.post("password", set_password, "set_password");
|
|
router.post("email", email_set, "email_set");
|
|
router.post("recover", recover_set, "recover_set");
|
|
|
|
// it is sent in an email...
|
|
router.get("email/confirm", email_confirm, "email_confirm");
|
|
router.get("recover", recover, "recover");
|
|
|
|
router
|
|
}
|
|
|
|
fn payment_mount() -> Router {
|
|
let mut router = Router::new();
|
|
router.post("stripe", stripe, "stripe");
|
|
|
|
router
|
|
}
|
|
|
|
pub fn start(pool: PgPool, mailer: SmtpTransport) {
|
|
let mut mounts = Mount::new();
|
|
|
|
mounts.mount("/api/account/", account_mount());
|
|
mounts.mount("/api/payments/", payment_mount());
|
|
mounts.mount("/api/acp/", acp::acp_mount());
|
|
|
|
let mut chain = Chain::new(mounts);
|
|
chain.link(Read::<State>::both(State { pool }));
|
|
chain.link(Write::<Mailer>::both(Mailer { mailer }));
|
|
chain.link_before(Read::<bodyparser::MaxBodyLength>::one(MAX_BODY_LENGTH));
|
|
chain.link_before(AuthMiddleware);
|
|
chain.link_after(ErrorHandler);
|
|
Iron::new(chain).http("127.0.0.1:40000").unwrap();
|
|
}
|