added registration/login/base guards

This commit is contained in:
Artemis 2025-01-25 15:41:51 +01:00
parent 35f35e3a81
commit 5f243c268a
10 changed files with 194 additions and 23 deletions

2
migrations/3_emails.sql Normal file
View file

@ -0,0 +1,2 @@
alter table users
add column email varchar(254);

View file

@ -1 +1,2 @@
pub mod pw;
pub mod session;

63
src/auth/session.rs Normal file
View 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))))
}

View file

@ -76,6 +76,7 @@ pub struct User {
pub username: String,
pub password: String,
pub email: Option<String>,
pub enabled: bool,
}

View file

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

View file

@ -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,
],
)
}

View file

@ -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("/")
}

View file

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

View file

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

View file

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