Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Users controller #55

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ members = [
"examples/request-tracking",
"examples/engine",
"rwf-admin",
"examples/files", "examples/users",
"examples/files", "examples/users", "rwf-auth",
]
exclude = ["examples/rails", "rwf-ruby", "examples/django", "rwf-fuzz"]
1 change: 1 addition & 0 deletions examples/users/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ edition = "2021"
rwf = { path = "../../rwf" }
time = "0.3"
argon2 = "0.5"
rwf-auth = { path = "../../rwf-auth" }
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE rwf_auth_users;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE rwf_auth_users (
id BIGSERIAL PRIMARY KEY,
identifier VARCHAR NOT NULL UNIQUE,
password VARCHAR NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW ()
);

CREATE INDEX ON rwf_auth_users USING btree (created_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS rwf_requests;

DROP TABLE IF EXISTS rwf_jobs;
45 changes: 45 additions & 0 deletions examples/users/migrations/1733778837089567000_rwf_init.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

CREATE TABLE IF NOT EXISTS rwf_jobs (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
args JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
start_after TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
attempts INT NOT NULL DEFAULT 0,
retries BIGINT NOT NULL DEFAULT 25,
completed_at TIMESTAMPTZ,
error VARCHAR
);

-- Pending jobs
CREATE INDEX IF NOT EXISTS rwf_jobs_pending_idx ON rwf_jobs USING btree(start_after, created_at) WHERE
completed_at IS NULL
AND started_at IS NULL
AND attempts < retries;

-- Running jobs
CREATE INDEX IF NOT EXISTS rwf_jobs_runnin_idx ON rwf_jobs USING btree(start_after, created_at) WHERE
completed_at IS NULL
AND started_at IS NOT NULL
AND attempts < retries;

CREATE INDEX IF NOT EXISTS rwf_jobs_name_completed_at_idx ON rwf_jobs USING btree(name, completed_at);

CREATE TABLE IF NOT EXISTS rwf_requests (
id BIGSERIAL PRIMARY KEY,
path VARCHAR NOT NULL,
method VARCHAR NOT NULL DEFAULT 'GET',
query JSONB NOT NULL DEFAULT '{}'::jsonb,
code INTEGER NOT NULL DEFAULT 200,
client_ip INET,
client_id UUID NOT NULL DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
duration REAL NOT NULL
);

CREATE INDEX IF NOT EXISTS rwf_requests_path_created_at ON rwf_requests USING btree(created_at, path, client_id);

CREATE INDEX IF NOT EXISTS rwf_requests_errors ON rwf_requests USING btree(created_at, code, client_id) WHERE code >= 400;

CREATE INDEX IF NOT EXISTS rwf_requests_too_slow ON rwf_requests USING btree(created_at, duration, client_id) WHERE duration >= 1000.0; -- the unit is milliseconds
2 changes: 1 addition & 1 deletion examples/users/rwf.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[general]
secret_key = "9Sk2t2G40QC3QrdVr6e6RzYAKJLGTFjpDiRrmA7eGQk="
log_queries = true
# log_queries = true

[database]
name = "rwf_users"
51 changes: 0 additions & 51 deletions examples/users/src/controllers.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,6 @@
use crate::models::*;
use rwf::prelude::*;

#[derive(macros::Form)]
struct SignupForm {
email: String,
password: String,
}

#[derive(Default, macros::PageController)]
pub struct Signup;

#[async_trait]
impl PageController for Signup {
async fn get(&self, request: &Request) -> Result<Response, Error> {
let user = request.user::<User>(Pool::pool()).await?;

if let Some(_) = user {
return Ok(Response::new().redirect("/profile"));
}

render!(request, "templates/signup.html")
}

async fn post(&self, request: &Request) -> Result<Response, Error> {
let form = request.form::<SignupForm>()?;
let user = User::signup(&form.email, &form.password).await?;

match user {
UserLogin::Ok(user) => Ok(request.login_user(&user)?.redirect("/profile")),
_ => render!(request, "templates/signup.html", "error" => true, 400),
}
}
}

#[controller]
pub async fn login(request: &Request) -> Result<Response, Error> {
let form = request.form::<SignupForm>()?;

let user = User::login(&form.email, &form.password).await?;

if let UserLogin::Ok(_) = user {
Ok(Response::new().redirect("/profile"))
} else {
render!(
request,
"templates/signup.html",
"login" => true,
"error" => true,
400
)
}
}

#[controller]
pub async fn profile(request: &Request) -> Result<Response, Error> {
let user = {
Expand Down
8 changes: 6 additions & 2 deletions examples/users/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use rwf::{http::Server, prelude::*};
use rwf_auth::controllers::{LogoutController, PasswordController};

mod controllers;
mod models;
Expand All @@ -8,8 +9,11 @@ async fn main() {
Logger::init();

Server::new(vec![
route!("/signup" => controllers::Signup),
route!("/login" => controllers::login),
route!("/auth" => {
PasswordController::template("templates/login.html")
.redirect("/profile")
}),
route!("/logout" => { LogoutController::redirect("/") }),
route!("/profile" => controllers::profile),
])
.launch()
Expand Down
68 changes: 2 additions & 66 deletions examples/users/src/models.rs
Original file line number Diff line number Diff line change
@@ -1,74 +1,10 @@
// use rwf::model::Error;
use rwf::crypto::{hash, hash_validate};
use rwf::prelude::*;
use tokio::task::spawn_blocking;

pub enum UserLogin {
NoSuchUser,
WrongPassword,
Ok(User),
}

#[derive(Clone, macros::Model)]
#[derive(Clone, macros::Model, macros::UserModel)]
#[user_model(email, password)]
pub struct User {
id: Option<i64>,
email: String,
password: String,
created_at: OffsetDateTime,
}

impl User {
/// Create new user with email and password.
pub async fn signup(email: &str, password: &str) -> Result<UserLogin, Error> {
let hash_password = password.to_owned();
let encrypted_password = spawn_blocking(move || hash(hash_password.as_bytes()))
.await
.unwrap()?;

match Self::login(email, password).await? {
UserLogin::Ok(user) => return Ok(UserLogin::Ok(user)),
UserLogin::WrongPassword => return Ok(UserLogin::WrongPassword),
_ => (),
}

let user = User::create(&[
("email", email.to_value()),
("password", encrypted_password.to_value()),
])
.fetch(Pool::pool())
.await?;

Ok(UserLogin::Ok(user))
}

/// Login user with email and password.
///
/// Return a user if one exists and the passwords match.
/// Return `None` otherwise.
pub async fn login(email: &str, password: &str) -> Result<UserLogin, Error> {
if let Some(user) = User::filter("email", email)
.fetch_optional(Pool::pool())
.await?
{
if hash_validate(password.as_bytes(), &user.password)? {
return Ok(UserLogin::Ok(user));
} else {
return Ok(UserLogin::WrongPassword);
}
}

Ok(UserLogin::NoSuchUser)
}
}

#[cfg(test)]
mod test {
use super::*;

#[tokio::test]
async fn test_user() {
Migrations::migrate().await.unwrap();
let _user = User::signup("test@test.com", "password2").await.unwrap();
let _user = User::login("test@test.com", "password2").await.unwrap();
}
}
42 changes: 42 additions & 0 deletions examples/users/templates/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!doctype html>
<html data-bs-theme="dark">
<head>
<%% "templates/head.html" %>
</head>
<body>
<div class="container pt-5 col-lg-6 col-12">
<h1 class="mb-4">Login</h1>
<form method="post" action="/auth">
<%= csrf_token() %>

<% if error_user_does_not_exist %>
<div class="alert alert-danger">
Account with this email doesn't exist or the password is incorrect.
</div>
<% end %>

<% if error_password %>
<div class="alert alert-danger">
Wrong password.
</div>
<% end %>

<div class="mb-3">
<label class="form-label">Email</label>
<input class="form-control" type="email" placeholder="Your email, e.g. user@example.com" required autocomplete="off" name="identifier">
</div>

<div class="mb-3">
<label class="form-label">Password</label>
<input class="form-control" type="password" placeholder="A secure password" required autocomplete="off" name="password">
</div>

<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">
Login
</button>
</div>
</form>
</div>
</body>
</html>
22 changes: 13 additions & 9 deletions examples/users/templates/signup.html
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
<!doctype html>
<html data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<%% "templates/head.html" %>
</head>
<body>
<div class="container pt-5">
<form method="post" action="/signup">
<% if error %>
<div class="container pt-5 col-lg-6 col-12">
<h1 class="mb-4">Create account</h1>
<form method="post" action="/auth">
<% if error_user_exists %>
<div class="alert alert-danger">
Account with this email already exists, and the password is incorrect.
Account with this email already exists.
</div>
<% end %>
<%= csrf_token() %>
<div class="mb-3">
<label class="form-label">Email</label>
<input class="form-control" type="email" placeholder="Your email, e.g. user@example.com" required autocomplete="off" name="email">
<input class="form-control" type="email" placeholder="Your email, e.g. user@example.com" required autocomplete="off" name="identifier">
</div>

<div class="mb-3">
<label class="form-label">Password</label>
<input class="form-control" type="password" placeholder="A secure password" required autocomplete="off" name="password">
</div>

<div class="mb-3">
<label class="form-label">Password again</label>
<input class="form-control" type="password" placeholder="A secure password" required autocomplete="off" name="password2">
</div>

<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">
Signup
Create account
</button>
</div>
</form>
Expand Down
8 changes: 8 additions & 0 deletions rwf-auth/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "rwf-auth"
version = "0.1.0"
edition = "2021"
include = ["migrations/", "src/", "static/", "templates/"]

[dependencies]
rwf = { path = "../rwf", version = "0.2.1" }
Empty file added rwf-auth/migrations/.gitkeep
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE rwf_auth_users;
8 changes: 8 additions & 0 deletions rwf-auth/migrations/1733765331409957000_rwf_auth_users.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE rwf_auth_users (
id BIGSERIAL PRIMARY KEY,
identifier VARCHAR NOT NULL UNIQUE,
password VARCHAR NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW ()
);

CREATE INDEX ON rwf_auth_users USING btree (created_at);
Loading
Loading