added registration/login/base guards
This commit is contained in:
parent
35f35e3a81
commit
5f243c268a
10 changed files with 194 additions and 23 deletions
2
migrations/3_emails.sql
Normal file
2
migrations/3_emails.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
alter table users
|
||||
add column email varchar(254);
|
|
@ -1 +1,2 @@
|
|||
pub mod pw;
|
||||
pub mod session;
|
||||
|
|
63
src/auth/session.rs
Normal file
63
src/auth/session.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use rocket::{
|
||||
http::{CookieJar, Status},
|
||||
request::{FromRequest, Outcome},
|
||||
response::Redirect,
|
||||
Request,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db::{
|
||||
schema::{DollTagsDb, User},
|
||||
user,
|
||||
};
|
||||
|
||||
pub fn login(cookies: &CookieJar<'_>, user_id: &Uuid) {
|
||||
cookies.add_private(("user_id", user_id.to_string()));
|
||||
}
|
||||
|
||||
pub fn check_login(cookies: &CookieJar<'_>) -> Option<Uuid> {
|
||||
cookies
|
||||
.get_private("user_id")
|
||||
.and_then(|v| Uuid::from_str(&v.value()).ok())
|
||||
}
|
||||
|
||||
pub fn logout(cookies: &CookieJar<'_>) {
|
||||
cookies.remove_private("user_id");
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SessionInternalFailure();
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'a> FromRequest<'a> for User {
|
||||
type Error = SessionInternalFailure;
|
||||
|
||||
async fn from_request(req: &'a Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let cookies = req.cookies();
|
||||
|
||||
if let Some(id) = check_login(&cookies) {
|
||||
let db = DollTagsDb::from_request(req)
|
||||
.await
|
||||
.expect("User::from_request cannot get DB connection");
|
||||
|
||||
match user::get_by_id(db, &id).await {
|
||||
Err(err) => {
|
||||
error!("User::from_request internal error: {:?}", err);
|
||||
Outcome::Error((Status::InternalServerError, SessionInternalFailure()))
|
||||
}
|
||||
Ok(Some(user)) => Outcome::Success(user),
|
||||
Ok(None) => Outcome::Forward(Status::Unauthorized),
|
||||
}
|
||||
} else {
|
||||
Outcome::Forward(Status::Unauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[catch(401)]
|
||||
pub fn unauthorized(req: &Request) -> Redirect {
|
||||
let next = req.uri().to_string();
|
||||
Redirect::to(uri!(crate::routes::form::accounts::show_login(Some(&next))))
|
||||
}
|
|
@ -76,6 +76,7 @@ pub struct User {
|
|||
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub email: Option<String>,
|
||||
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
|
|
@ -10,15 +10,23 @@ pub async fn get(mut db: DollTagsDb, username: &str) -> sqlx::Result<Option<User
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn get_by_id(mut db: DollTagsDb, id: &Uuid) -> sqlx::Result<Option<User>> {
|
||||
sqlx::query_as!(User, "select * from users where id = $1", id)
|
||||
.fetch_optional(&mut **db)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
mut db: DollTagsDb,
|
||||
username: &str,
|
||||
hashed_password: &str,
|
||||
email: Option<&str>,
|
||||
) -> sqlx::Result<Uuid> {
|
||||
sqlx::query_scalar!(
|
||||
"insert into users (username, password) values ($1, $2) returning id",
|
||||
"insert into users (username, password, email) values ($1, $2, $3) returning id",
|
||||
username,
|
||||
hashed_password
|
||||
hashed_password,
|
||||
email,
|
||||
)
|
||||
.fetch_one(&mut **db)
|
||||
.await
|
||||
|
|
15
src/main.rs
15
src/main.rs
|
@ -2,8 +2,9 @@
|
|||
extern crate rocket;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use auth::session;
|
||||
use db::migrate::run_migrations;
|
||||
use db::schema::DollTags;
|
||||
use db::schema::{DollTags, User};
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket::fs::{relative, FileServer};
|
||||
use rocket_db_pools::Database;
|
||||
|
@ -18,6 +19,11 @@ pub mod db;
|
|||
pub mod ids;
|
||||
pub mod routes;
|
||||
|
||||
#[get("/meow")]
|
||||
pub fn test_route(woof: User) -> &'static str {
|
||||
"Woof"
|
||||
}
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
rocket::build()
|
||||
|
@ -31,7 +37,10 @@ fn rocket() -> _ {
|
|||
}))
|
||||
.attach(DollTags::init())
|
||||
.attach(AdHoc::try_on_ignite("SQLx migrations", run_migrations))
|
||||
.register("/", catchers![error_handlers::not_found])
|
||||
.register(
|
||||
"/",
|
||||
catchers![error_handlers::not_found, session::unauthorized],
|
||||
)
|
||||
.mount("/assets", FileServer::from(relative!("/assets")))
|
||||
.mount(
|
||||
"/",
|
||||
|
@ -44,6 +53,8 @@ fn rocket() -> _ {
|
|||
accounts::handle_register,
|
||||
accounts::show_login,
|
||||
accounts::handle_login,
|
||||
accounts::logout,
|
||||
test_route,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use regex::Regex;
|
||||
use rocket::{
|
||||
form::{Contextual, Form},
|
||||
form::{self, Contextual, Form},
|
||||
http::CookieJar,
|
||||
response::Redirect,
|
||||
tokio::task,
|
||||
|
@ -7,7 +8,7 @@ use rocket::{
|
|||
use rocket_dyn_templates::{context, Template};
|
||||
|
||||
use crate::{
|
||||
auth::pw,
|
||||
auth::{pw, session},
|
||||
db::{schema::DollTagsDb, user},
|
||||
routes::error_handlers::{PageResponse, PageResult},
|
||||
};
|
||||
|
@ -20,14 +21,17 @@ pub struct AuthForm<'a> {
|
|||
pub password: &'a str,
|
||||
}
|
||||
|
||||
#[get("/login")]
|
||||
pub fn show_login() -> Template {
|
||||
// needed cuz the var. name changes how the query string is parsed
|
||||
#[allow(unused_variables)]
|
||||
#[get("/login?<next>")]
|
||||
pub fn show_login(next: Option<&str>) -> Template {
|
||||
Template::render("account/login", context! {})
|
||||
}
|
||||
|
||||
#[post("/login", data = "<form>")]
|
||||
#[post("/login?<next>", data = "<form>")]
|
||||
pub async fn handle_login(
|
||||
db: DollTagsDb,
|
||||
next: Option<&str>,
|
||||
form: Form<Contextual<'_, AuthForm<'_>>>,
|
||||
cookies: &CookieJar<'_>,
|
||||
) -> PageResult {
|
||||
|
@ -49,8 +53,9 @@ pub async fn handle_login(
|
|||
task::spawn_blocking(move || pw::verify(&password, &user.password)).await??;
|
||||
|
||||
if right_password && user.enabled {
|
||||
cookies.add_private(("user_id", user.id.to_string()));
|
||||
Ok(Redirect::to("/").into())
|
||||
session::login(cookies, &user.id);
|
||||
let next_url = String::from(next.unwrap_or("/"));
|
||||
Ok(Redirect::to(next_url).into())
|
||||
} else {
|
||||
Ok(miss())
|
||||
}
|
||||
|
@ -58,14 +63,94 @@ pub async fn handle_login(
|
|||
|
||||
#[get("/register")]
|
||||
pub fn show_register() -> Template {
|
||||
Template::render("account/register", context! {})
|
||||
Template::render(
|
||||
"account/register",
|
||||
context! {
|
||||
previous: form::Context::default(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[post("/register", data = "<_form>")]
|
||||
pub async fn handle_register(
|
||||
_db: DollTagsDb,
|
||||
_form: Form<AuthForm<'_>>,
|
||||
_cookies: &CookieJar<'_>,
|
||||
) -> PageResult {
|
||||
todo!("meow")
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct RegisterForm<'a> {
|
||||
#[field(validate=len(1..=256))]
|
||||
pub username: &'a str,
|
||||
#[field(validate=len(8..))]
|
||||
pub password: &'a str,
|
||||
#[field(validate=validate_email())]
|
||||
pub email: &'a str,
|
||||
|
||||
// if it's set to true, a (bad) bot is submitting the form since it's a hidden field
|
||||
pub anti_bot: bool,
|
||||
// if it isn't set to true, we re-request it to be set
|
||||
pub inhuman: bool,
|
||||
}
|
||||
|
||||
fn validate_email<'v>(email: &str) -> form::Result<'v, ()> {
|
||||
let email_re = Regex::new(r".+@.+").unwrap();
|
||||
if email.len() == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if email.len() >= 3 && email.len() <= 254 && email_re.is_match(email) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(form::Error::validation("this doesn't appear to be a valid email").into())
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/register", data = "<form>")]
|
||||
pub async fn handle_register(
|
||||
db: DollTagsDb,
|
||||
form: Form<Contextual<'_, RegisterForm<'_>>>,
|
||||
cookies: &CookieJar<'_>,
|
||||
) -> PageResult {
|
||||
let values = match form.value {
|
||||
Some(ref v) => v,
|
||||
None => {
|
||||
debug!(
|
||||
"account registration form invalid, context: {:?}",
|
||||
&form.context
|
||||
);
|
||||
|
||||
return Ok(Template::render(
|
||||
"account/register",
|
||||
context! {
|
||||
previous: &form.context,
|
||||
},
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
"registering account {} ({:?})",
|
||||
values.username, values.email
|
||||
);
|
||||
|
||||
let password = String::from(values.password);
|
||||
let hashed_password = task::spawn_blocking(move || pw::hash(&password)).await??;
|
||||
|
||||
let account_id = user::create(
|
||||
db,
|
||||
values.username,
|
||||
&hashed_password,
|
||||
if values.email.len() != 0 {
|
||||
Some(values.email)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
session::login(cookies, &account_id);
|
||||
|
||||
Ok(Redirect::to(uri!("/")).into())
|
||||
}
|
||||
|
||||
#[get("/logout")]
|
||||
pub fn logout(cookies: &CookieJar<'_>) -> Redirect {
|
||||
session::logout(cookies);
|
||||
|
||||
Redirect::to("/")
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ pub async fn handle_register(
|
|||
// in case the form validation fails, this will be tasked with rendering the page again with submitted values and display errors
|
||||
let ids = pick_ids(db).await?;
|
||||
|
||||
debug!("registration form invalid, context: {:?}", &tag.context);
|
||||
debug!("tag registration form invalid, context: {:?}", &tag.context);
|
||||
|
||||
return Ok(Template::render(
|
||||
"register_tag",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base" %}
|
||||
{% block title %}Log in - {% endblock title %}
|
||||
{% block main %}
|
||||
<form action="/profile" id="stray-form">
|
||||
<form method="post">
|
||||
<h2>Log in</h2>
|
||||
|
||||
{% if failure %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base" %}
|
||||
{% block title %}Register a new account - {% endblock title %}
|
||||
{% block main %}
|
||||
<form action="/profile" id="stray-form">
|
||||
<form method="post">
|
||||
<h2>Register a new account</h2>
|
||||
|
||||
{% if failure %}
|
||||
|
@ -21,7 +21,7 @@
|
|||
|
||||
<div>
|
||||
<p class="heading"><label for="email">Optionally, add a recovery e-mail</label></p>
|
||||
<input type="email" name="email" id="email" placeholder="woof@example.com">
|
||||
<input type="email" name="email" id="email" maxlength="254" placeholder="woof@example.com">
|
||||
<p>
|
||||
This e-mail is fully optional and will only be used if you were to lose your password.
|
||||
If you lose your password and haven't set an e-mail, you may still send me an e-mail and we'll see
|
||||
|
|
Loading…
Add table
Reference in a new issue