TWIZ7QV4GCTQK743IKZSOJCIAEX62GZHFIYGOIFCFGIBOGPSY2WAC
{% 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 %}
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 user
name: 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]
}
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 id
Ok(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)
}
}
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)
}
}
-- Add up migration script here
CREATE 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);
-- Add down migration script here
DROP TABLE IF EXISTS projects;