routes: Move user routes to controllers module The last routes to move, the src directory now only has a main.rs file and directories.
Dependencies
- [2]
F5DMFQAOroutes: Move project routes to controllers module Much like other routes, just move the files and clean up the tree. - [3]
T7TT5B4Gmodels: Put User model in their own file Before adding a next model, let's start organizing the code a little, to make extending easier. - [4]
TWIZ7QV4db: Add interface to add a project Right now a project has a name, and an owner which is hardcoded to 1. This is because basically I'm speedrunning to implement push/pull of Pijul and then revisit to add depth to features and tests. Model code is now split into files properly too. - [5]
W3M3C7CCInitial commit This change includes a very small hello world application server written in Rust using Rocket.rs. Managing dependencies is done with Nix as that works well between Linux and Mac for me. - [6]
DSWQKJRHusers: Introduce User guard for routes Rockets guards are very powerful to disallow users for certain routes. This far this wasn't implemented, and allowed no-one other than the first user to sign up. This change introduces the User guard and employs it for a few routes. The guard works by checking the encrypted cookie for the user_id, and perform a database lookup on it. - [7]
ZGXZ2IRICleanup leftover files Recorded earlier, but uninteded. Now it's cleaned up again. - [8]
5UNA2DEAroutes: Register and authenticate users Allow users to sign up, and sign in/sign out. The routes are added, though the design of the pages is very bare bones still, it's hard to go through the full flow to demo. On the server side: Passwords are stored encrypted in the database with salts. This uses the PG encrypt tooling to prevent against bugs and maintainance costs on this project. When a user is signed in, the user ID is set in a private cookie. Rocket has Guards for routes, which has not been implemented yet for this project. - [9]
FS2NWBVNpijul: Start of push/pull work This change includes one API endpoint, .pijul. It allows for getting a channels remote ID. A lot of plumbing around repositories is added too, from init to opening pristine and actions like it. - [10]
KFVJ3KMWfrontend: Introduce navigation bar Minor changes to the front-end mostly, to allow users to register, sign in, and sign out. The sign out route is changed to a GET endpoint, as links in HTML cannot DELETE. - [11]
KULVODXDroutes: User route mod exposes routes() now Other routes already did this to keep the routes clean when mounting. Now the user routes do too. - [12]
3E77DEMDroutes: Move root route to controllers module The routes were now in modules in the root, which created a messy situation for the future. For now the routes will be moved to the controllers module and later additional changes will be done to further clean this up. - [13]
K4JNAJOFdatabase: Connect to postgres on Rocket boot As database I've chosen PostgreSQL, as my personal experience has been good with it. This change allows Rocket to connect to the database on booting the server. It depends on the DATABASE_URL being set, and for now circumvents the Rocket config helpers as it seemed faster to be up and running this way. - [14]
S6TFYMRGroutes: Move pijul routes to controllers module As with the root routes, now the pijul routes are moved. The mounting of the routes is still done in main.rs though the controller module now collects them. This should DRY this code
Change contents
- file deletion: users.rs
use rocket::{form::{Context, Form},response::{Flash, Redirect},};use rocket_dyn_templates::Template;use crate::database::Database;fn new() -> Template {Template::render("new_user", &Context::default())}#[derive(FromForm)]struct NewUser {// TODO validate against regexp: [a-z_][a-z0-9_-]*[$] - Needs a valid linux username#[field(validate = len(..20))]pub user_name: String,#[field(validate = len(3..))]pub email: String,#[field(validate = len(6..64))]pub password: String,}#[post("/", data = "<user>")]async fn create(db: &State<Database>,cookie: &CookieJar<'_>,user: Form<NewUser>,) -> Result<Flash<Redirect>, Flash<Redirect>> {// TODO When the form parsing fails, the users gets no feedbacklet mut new_user = User {id: -1,name: user.user_name.clone(),email: user.email.clone(),password: user.password.clone(),};match new_user.create(db).await {Ok(id) => {new_user.id = id;set_user_cookie(cookie, new_user);return Ok(Flash::success(Redirect::to("/"), "Signed up succesfully"));}Err(_e) => Err(Flash::error(Redirect::to("/users/new"),"Something went wrong",)), //TODO Show the error to the user in a flash message,}}#[derive(FromForm)]struct SignIn {#[field(validate = len(..20))]pub user_name: String,#[field(validate = len(6..64))]pub password: String,}#[get("/sign_in")]async fn get_sign_in() -> Template {Template::render("sign_in", &Context::default())}#[post("/sign_in", data = "<user>")]async fn sign_in(db: &State<Database>,jar: &CookieJar<'_>,user: Form<SignIn>,) -> Result<Flash<Redirect>, Flash<Redirect>> {// TODO figure out Rust and rewrite this without the nested matchingmatch User::authenticate(db, user.user_name.clone(), user.password.clone()).await {Ok(u) => match u {Some(u2) => {set_user_cookie(jar, u2);return Ok(Flash::success(Redirect::to("/"), "Signed in!"));}None => return Err(Flash::error(Redirect::to("./sign_in"), "Error signing in")),},Err(_e) => {return Err(Flash::error(Redirect::to("./sign_in"),"SQL Error signing in",))}}}static COOKIE_USER_ID_KEY: &str = "user_id";#[get("/sign_out")]fn sign_out(jar: &CookieJar<'_>) -> Flash<Redirect> {jar.remove_private(Cookie::named(COOKIE_USER_ID_KEY));Flash::success(Redirect::to("/"), "Signed out succesfully")}fn set_user_cookie(jar: &CookieJar<'_>, user: User) {jar.add_private(Cookie::new(COOKIE_USER_ID_KEY, user.id.to_string()))}pub fn routes() -> Vec<Route> {}routes![new,new_already_user,create,get_sign_in,sign_in,sign_out]}fn get_user_id_from_cookie(jar: &CookieJar) -> Option<i32> {match jar.get_private(COOKIE_USER_ID_KEY) {Some(id) => Some(id.value().parse::<i32>().ok()?),None => None,}#[get("/new", rank = 1)]fn new_already_user(_user: User) -> Redirect {Redirect::to("/")}// TODO decide if this routing file is the place for this guard to be at?#[rocket::async_trait]impl<'r> FromRequest<'r> for User {type Error = ();async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {let db = try_outcome!(request.guard::<&State<Database>>().await);// &State<Database>>().await);let cookies = request.cookies();let some_id = match get_user_id_from_cookie(cookies) {Some(id) => id,None => return Outcome::Failure((Status::Unauthorized, ())),};match User::find(db, some_id).await {Ok(u) => Outcome::Success(u),Err(_e) => Outcome::Failure((Status::Unauthorized, ())),}}}#[get("/new", rank = 2)]use crate::models::users::User;use rocket::outcome::try_outcome;Request, Route, State,http::{Cookie, CookieJar, Status},request,request::{FromRequest, Outcome}, - edit in src/main.rs at line 14
mod users; - edit in src/main.rs at line 24
.mount("/users", users::routes()) - file addition: users.rs[3.85]
use rocket::{form::{Context, Form},http::{Cookie, CookieJar, Status},request,request::{FromRequest, Outcome},response::{Flash, Redirect},Request, Route, State,};use rocket_dyn_templates::Template;use crate::database::Database;use crate::models::users::User;use rocket::outcome::try_outcome;// TODO decide if this routing file is the place for this guard to be at?#[rocket::async_trait]impl<'r> FromRequest<'r> for User {type Error = ();async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {let db = try_outcome!(request.guard::<&State<Database>>().await);// &State<Database>>().await);let cookies = request.cookies();let some_id = match get_user_id_from_cookie(cookies) {Some(id) => id,None => return Outcome::Failure((Status::Unauthorized, ())),};match User::find(db, some_id).await {Ok(u) => Outcome::Success(u),Err(_e) => Outcome::Failure((Status::Unauthorized, ())),}}}#[get("/users/new", rank = 2)]fn new() -> Template {Template::render("new_user", &Context::default())}#[get("/users/new", rank = 1)]fn new_already_user(_user: User) -> Redirect {Redirect::to("/")}#[derive(FromForm)]struct NewUser {// TODO validate against regexp: [a-z_][a-z0-9_-]*[$] - Needs a valid linux username#[field(validate = len(..20))]pub user_name: String,#[field(validate = len(3..))]pub email: String,#[field(validate = len(6..64))]pub password: String,}#[post("/users", data = "<user>")]async fn create(db: &State<Database>,cookie: &CookieJar<'_>,user: Form<NewUser>,) -> Result<Flash<Redirect>, Flash<Redirect>> {// TODO When the form parsing fails, the users gets no feedbacklet mut new_user = User {id: -1,name: user.user_name.clone(),email: user.email.clone(),password: user.password.clone(),};match new_user.create(db).await {Ok(id) => {new_user.id = id;set_user_cookie(cookie, new_user);return Ok(Flash::success(Redirect::to("/"), "Signed up succesfully"));}Err(_e) => Err(Flash::error(Redirect::to("/users/new"),"Something went wrong",)), //TODO Show the error to the user in a flash message,}}#[derive(FromForm)]struct SignIn {#[field(validate = len(..20))]pub user_name: String,#[field(validate = len(6..64))]pub password: String,}#[get("/users/sign_in")]async fn get_sign_in() -> Template {Template::render("sign_in", &Context::default())}#[post("/users/sign_in", data = "<user>")]async fn sign_in(db: &State<Database>,jar: &CookieJar<'_>,user: Form<SignIn>,) -> Result<Flash<Redirect>, Flash<Redirect>> {// TODO figure out Rust and rewrite this without the nested matchingmatch User::authenticate(db, user.user_name.clone(), user.password.clone()).await {Ok(u) => match u {Some(u2) => {set_user_cookie(jar, u2);return Ok(Flash::success(Redirect::to("/"), "Signed in!"));}None => return Err(Flash::error(Redirect::to("./sign_in"), "Error signing in")),},Err(_e) => {return Err(Flash::error(Redirect::to("./sign_in"),"SQL Error signing in",))}}}static COOKIE_USER_ID_KEY: &str = "user_id";#[get("/users/sign_out")]fn sign_out(jar: &CookieJar<'_>) -> Flash<Redirect> {jar.remove_private(Cookie::named(COOKIE_USER_ID_KEY));Flash::success(Redirect::to("/"), "Signed out succesfully")}fn set_user_cookie(jar: &CookieJar<'_>, user: User) {jar.add_private(Cookie::new(COOKIE_USER_ID_KEY, user.id.to_string()))}fn get_user_id_from_cookie(jar: &CookieJar) -> Option<i32> {match jar.get_private(COOKIE_USER_ID_KEY) {Some(id) => Some(id.value().parse::<i32>().ok()?),None => None,}}pub fn routes() -> Vec<Route> {routes![new,new_already_user,create,get_sign_in,sign_in,sign_out]} - edit in src/controllers/mod.rs at line 6
mod users; - replacement in src/controllers/mod.rs at line 9
[root::routes(), pijul::routes(), projects::routes()].concat()[root::routes(),pijul::routes(),projects::routes(),users::routes(),].concat()