db: 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.
Dependencies
- [2]
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. - [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. - [*]
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. - [*]
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. - [*]
IWM4EE63database: Add migration support Fairly minor change codewise; add support for creating migrations and run them at the start of the runtime. In this case not a lot of changes happen, only the migrations tracking table is added. The sqlx command line tool is used to manage migrations; invoke `cargo sqlx --help` for more info.
Change contents
- file addition: projects[5.21]
- file addition: new.html.tera[0.20]
{% extends "base" %}{% block body %}<div style="width:30%;" class="container-fluid"><form action="/projects" method="post"><div class="mb-3 row"><label for="user_name" class="col-sm-2 col-form-label">Name</label><div class="col-sm-10"><input type="text" class="form-control" name="name" required></div></div><button class="btn btn-primary" type="submit">Create!</button></form></div>{% endblock body %} - file addition: projects.rs[6.179]
use crate::database::Database;use crate::models::projects::Project;use lazy_static::lazy_static;use regex::Regex;use rocket::{form::{self, Context, Error, Form},response::{Flash, Redirect},Route, State,};use rocket_dyn_templates::Template;#[get("/new")]fn new() -> Template {Template::render("projects/new", &Context::default())}fn validate_project_name<'v>(name: &String) -> form::Result<'v, ()> {lazy_static! {static ref RE: Regex = Regex::new(r"\A[\w\d]{2,20}\z").unwrap();}if !RE.is_match(&name) {Err(Error::validation("only up to 20 letters or digets are to be used as project name",))?;}Ok(())}#[derive(FromForm)]struct NewProject {#[field(validate = validate_project_name())]name: String,}#[post("/", data = "<project>")]async fn create(db: &State<Database>,project: Form<NewProject>,) -> Result<Flash<Redirect>, Flash<Redirect>> {let proj = Project {id: -1,owner_id: 1, // TODO replace this with username: project.name.clone(),};match proj.create(db).await {Ok(_) => Ok(Flash::success(Redirect::to("/"), "Project is created")),Err(_e) => Err(Flash::error(Redirect::to("/projects/new"),"Something went wrong",)),}}pub fn routes() -> Vec<Route> {routes![new, create]} - file addition: users.rs[5.6633]
use crate::database::Database;use rocket::serde::{Deserialize, Serialize};use rocket::State;use sqlx::{query, query_as};#[derive(Debug, Clone, Deserialize, Serialize)]#[serde(crate = "rocket::serde")]pub struct User {pub id: i32,pub name: String,pub email: String,#[serde(skip_deserializing)]pub password: String,}impl User {pub async fn create(&self, db: &State<Database>) -> Result<i32, sqlx::Error> {let result = query!("INSERT INTO users (name, email, password) VALUES ($1, $2, crypt($3, gen_salt('bf'))) RETURNING id",&self.name,&self.email.to_lowercase(),&self.password).fetch_one(&**db).await?;// TODO when I figure out Rust, update self with this idOk(result.id)}/// Validates a user and password combination, returns a User struct when/// valid.pub async fn authenticate(db: &State<Database>,name: String,password: String,) -> Result<Option<User>, sqlx::Error> {let result = query_as!(User,"SELECT * FROM users WHERE name = $1 AND password = crypt($2, password)",name,password).fetch_optional(&**db).await?;Ok(result)}} - file addition: projects.rs[5.6633]
use crate::database::Database;use rocket::serde::{Deserialize, Serialize};use rocket::State;use sqlx::query;#[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)]#[serde(crate = "rocket::serde")]pub struct Project {pub id: i32,pub owner_id: i32,pub name: String,}impl Project {pub async fn create(&self, db: &State<Database>) -> Result<i32, sqlx::Error> {let result = query!("INSERT INTO projects (owner_id, name) VALUES ($1, $2) RETURNING id",&self.owner_id,&self.name.to_lowercase(),).fetch_one(&**db).await?;Ok(result.id)}} - edit in src/models/mod.rs at line 1
pub mod projects; - edit in src/main.rs at line 11
mod projects; - edit in src/main.rs at line 30
.mount("/projects", projects::routes()) - file addition: 20210907120846_projects_table.up.sql[7.199]
-- Add up migration script hereCREATE TABLE IF NOT EXISTS projects (id SERIAL PRIMARY KEY,owner_id integer REFERENCES users,name VARCHAR(64) NOT NULL UNIQUE);CREATE UNIQUE INDEX unique_project_name ON projects(name); - file addition: 20210907120846_projects_table.down.sql[7.199]
-- Add down migration script hereDROP TABLE IF EXISTS projects; - edit in Cargo.toml at line 9
lazy_static = "1.4.0"regex = "1" - edit in Cargo.lock at line 1249
"lazy_static","regex",