312 lines
9.4 KiB
Rust
312 lines
9.4 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::{typemap, BeforeMiddleware,AfterMiddleware};
|
|
use persistent::Read;
|
|
use router::Router;
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
use account;
|
|
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="account name taken or invalid")]
|
|
AccountNameNotProvided,
|
|
#[fail(display="account name not provided")]
|
|
AccountNotFound,
|
|
#[fail(display="account not found")]
|
|
AccountNameTaken,
|
|
#[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,
|
|
#[fail(display="invalid code. https://discord.gg/YJJgurM")]
|
|
InvalidCode,
|
|
}
|
|
|
|
impl From<bcrypt::BcryptError> for MnmlHttpError {
|
|
fn from(_err: bcrypt::BcryptError) -> Self {
|
|
MnmlHttpError::ServerError
|
|
}
|
|
}
|
|
|
|
impl From<postgres::Error> for MnmlHttpError {
|
|
fn from(_err: postgres::Error) -> Self {
|
|
MnmlHttpError::DbError
|
|
}
|
|
}
|
|
|
|
impl From<failure::Error> for MnmlHttpError {
|
|
fn from(_err: failure::Error) -> Self {
|
|
MnmlHttpError::ServerError
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct JsonResponse {
|
|
response: Option<String>,
|
|
success: bool,
|
|
error_message: Option<String>
|
|
}
|
|
|
|
impl JsonResponse {
|
|
fn success(response: String) -> Self {
|
|
JsonResponse { response: Some(response), success: true, error_message: None }
|
|
}
|
|
|
|
fn error(msg: String) -> Self {
|
|
JsonResponse { response: None, success: false, error_message: Some(msg) }
|
|
}
|
|
}
|
|
|
|
fn iron_response (status: status::Status, message: String) -> Response {
|
|
let content_type = "application/json".parse::<Mime>().unwrap();
|
|
let msg = match status {
|
|
status::Ok => JsonResponse::success(message),
|
|
_ => JsonResponse::error(message)
|
|
};
|
|
let msg_out = serde_json::to_string(&msg).unwrap();
|
|
return Response::with((content_type, status, msg_out));
|
|
}
|
|
|
|
impl From<MnmlHttpError> for IronError {
|
|
fn from(m_err: MnmlHttpError) -> Self {
|
|
let (err, res) = match m_err {
|
|
MnmlHttpError::ServerError |
|
|
MnmlHttpError::DbError => (m_err.compat(), status::InternalServerError),
|
|
|
|
MnmlHttpError::AccountNameNotProvided |
|
|
MnmlHttpError::AccountNameTaken |
|
|
MnmlHttpError::AccountNotFound |
|
|
MnmlHttpError::BadRequest |
|
|
MnmlHttpError::PasswordUnacceptable => (m_err.compat(), status::BadRequest),
|
|
|
|
MnmlHttpError::PasswordNotMatch |
|
|
MnmlHttpError::InvalidCode |
|
|
MnmlHttpError::TokenDoesNotMatch |
|
|
MnmlHttpError::Unauthorized => (m_err.compat(), status::Unauthorized),
|
|
};
|
|
IronError { error: Box::new(err), response: iron_response(res, m_err.to_string()) }
|
|
}
|
|
}
|
|
|
|
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(IronError::from(MnmlHttpError::TokenDoesNotMatch)),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
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 = iron_response(status::Ok, "token_res".to_string());
|
|
res.headers.set(SetCookie(vec![v.to_string()]));
|
|
|
|
return res;
|
|
}
|
|
|
|
|
|
#[derive(Debug,Clone,Deserialize)]
|
|
struct RegisterBody {
|
|
name: String,
|
|
password: String,
|
|
code: 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(IronError::from(MnmlHttpError::BadRequest)),
|
|
};
|
|
|
|
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, ¶ms.code, &mut tx) {
|
|
Ok(token) => {
|
|
tx.commit().or(Err(MnmlHttpError::ServerError))?;
|
|
Ok(token_res(token))
|
|
},
|
|
Err(e) => {
|
|
warn!("{:?}", e);
|
|
Err(IronError::from(e))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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(IronError::from(MnmlHttpError::BadRequest)),
|
|
};
|
|
|
|
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(IronError::from(e))
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = iron_response(status::Ok, "logout".to_string());
|
|
res.headers.set(SetCookie(vec![AUTH_CLEAR.to_string()]));
|
|
Ok(res)
|
|
|
|
},
|
|
None => Err(IronError::from(MnmlHttpError::Unauthorized)),
|
|
}
|
|
}
|
|
|
|
#[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(IronError::from(MnmlHttpError::BadRequest)),
|
|
};
|
|
|
|
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(IronError::from(MnmlHttpError::Unauthorized)),
|
|
}
|
|
}
|
|
|
|
const MAX_BODY_LENGTH: usize = 1024 * 1024 * 10;
|
|
|
|
pub struct State {
|
|
pub pool: PgPool,
|
|
// pub events: Events,
|
|
}
|
|
|
|
impl Key for State { type Value = State; }
|
|
|
|
pub fn start(pool: PgPool) {
|
|
let mut router = Router::new();
|
|
|
|
// auth
|
|
router.post("/api/account/login", login, "login");
|
|
router.post("/api/account/logout", logout, "logout");
|
|
router.post("/api/account/register", register, "register");
|
|
router.post("/api/account/password", set_password, "set_password");
|
|
router.post("/api/account/email", logout, "email");
|
|
|
|
// payments
|
|
router.post("/api/payments/stripe", stripe, "stripe");
|
|
|
|
let mut chain = Chain::new(router);
|
|
chain.link(Read::<State>::both(State { pool }));
|
|
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();
|
|
}
|