Skip to content

Commit

Permalink
Users controller
Browse files Browse the repository at this point in the history
  • Loading branch information
levkk committed Dec 6, 2024
1 parent c21393b commit b7359b3
Show file tree
Hide file tree
Showing 15 changed files with 346 additions and 26 deletions.
6 changes: 5 additions & 1 deletion examples/users/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use rwf::controller::LoginController;
use rwf::{http::Server, prelude::*};

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

let signup: LoginController<models::User> =
LoginController::new("templates/signup.html").redirect("/profile");

Server::new(vec![
route!("/signup" => controllers::Signup),
route!("/signup" => { signup }),
route!("/login" => controllers::login),
route!("/profile" => controllers::profile),
])
Expand Down
11 changes: 10 additions & 1 deletion examples/users/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@ use rwf::crypto::{hash, hash_validate};
use rwf::prelude::*;
use tokio::task::spawn_blocking;

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

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,
Expand Down
19 changes: 14 additions & 5 deletions examples/users/templates/signup.html
Original file line number Diff line number Diff line change
@@ -1,27 +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/[email protected]/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 %>
<% if error_user_exists %>
<div class="alert alert-danger">
Account with this email already exists, and the password is incorrect.
</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. [email protected]" required autocomplete="off" name="email">
<input class="form-control" type="email" placeholder="Your email, e.g. [email protected]" required autocomplete="off" name="identifier">
<% if error_identifier %>
<div class="invalid-feedback">
Provided email is not valid.
</div>
<% end %>
</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">

<% if error_password %>
<div class="invalid-feedback">
Provided password is not valid.
</div>
<% end %>
</div>

<div class="d-flex justify-content-end">
Expand Down
29 changes: 10 additions & 19 deletions rwf-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
extern crate proc_macro;

use proc_macro::TokenStream;

use syn::{
parse_macro_input, punctuated::Punctuated, Attribute, Data, DeriveInput, Expr, ItemFn, Meta,
ReturnType, Token, Type, Visibility,
};

use quote::quote;

mod model;
mod prelude;
mod render;
mod route;
mod user;

use prelude::*;

/// The `#[derive(Model)]` macro.
///
Expand Down Expand Up @@ -522,16 +517,7 @@ pub fn error(input: TokenStream) -> TokenStream {
/// ```
#[proc_macro]
pub fn route(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input with Punctuated<Expr, Token![=>]>::parse_terminated);
let mut iter = input.into_iter();

let route = iter.next().unwrap();
let controller = iter.next().unwrap();

quote! {
#controller::default().route(#route)
}
.into()
route::route_impl(input)
}

/// Create CRUD routes for the controller.
Expand Down Expand Up @@ -737,3 +723,8 @@ pub fn controller(_args: TokenStream, input: TokenStream) -> TokenStream {
}
.into()
}

#[proc_macro_derive(UserModel, attributes(user_model))]
pub fn derive_user_model(input: TokenStream) -> TokenStream {
user::impl_derive_user_model(input)
}
1 change: 1 addition & 0 deletions rwf-macros/src/prelude.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub use proc_macro::TokenStream;
pub use quote::quote;
pub use syn::parse::*;
pub use syn::punctuated::Punctuated;
pub use syn::*;
34 changes: 34 additions & 0 deletions rwf-macros/src/route.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use crate::prelude::*;

struct RouteInput {
route: Expr,
controller: Expr,
}

impl Parse for RouteInput {
fn parse(input: ParseStream) -> Result<Self> {
let route = input.parse()?;
let _arrow: Token![=>] = input.parse()?;

let controller = input.parse()?;

Ok(Self { route, controller })
}
}

pub(crate) fn route_impl(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as RouteInput);

let route = input.route;
let controller = input.controller;

let controller = match controller {
Expr::Path(expr) => quote! { #expr::default() },
expr => quote! { #expr },
};

quote! {
#controller.route(#route)
}
.into()
}
52 changes: 52 additions & 0 deletions rwf-macros/src/user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use super::prelude::*;

struct UserModel {
identifier: Ident,
password: Ident,
}

impl Parse for UserModel {
fn parse(input: parse::ParseStream) -> Result<Self> {
let identifier: Ident = input.parse()?;
let _: Token![,] = input.parse()?;
let password = input.parse()?;

Ok(UserModel {
identifier,
password,
})
}
}

pub(crate) fn impl_derive_user_model(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ident = &input.ident;

if let Some(attr) = input.attrs.first() {
match attr.meta {
Meta::List(ref attrs) => {
let attrs = syn::parse2::<UserModel>(attrs.tokens.clone()).unwrap();

let identifier = attrs.identifier.to_string();
let password = attrs.password.to_string();

return quote! {
impl rwf::model::UserModel for #ident {
fn identifier_column() -> &'static str {
#identifier
}

fn password_column() -> &'static str {
#password
}
}
}
.into();
}

_ => (),
}
}

quote! {}.into()
}
3 changes: 3 additions & 0 deletions rwf/src/controller/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ pub enum Error {

#[error("timeout exceeded")]
TimeoutError(#[from] tokio::time::error::Elapsed),

#[error("user error: {0}")]
UserError(#[from] crate::model::user::Error),
}

impl Error {
Expand Down
2 changes: 2 additions & 0 deletions rwf/src/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub mod middleware;
pub mod ser;
pub mod static_files;
pub mod turbo_stream;
pub mod user;

#[cfg(feature = "wsgi")]
pub mod wsgi;
Expand All @@ -50,6 +51,7 @@ pub use error::Error;
pub use middleware::{Middleware, MiddlewareHandler, MiddlewareSet, Outcome, RateLimiter};
pub use static_files::{CacheControl, StaticFiles};
pub use turbo_stream::TurboStream;
pub use user::LoginController;

use super::http::{
websocket::{self, DataFrame},
Expand Down
85 changes: 85 additions & 0 deletions rwf/src/controller/user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use std::marker::PhantomData;

use super::{Controller, Error, PageController};
use crate::view::{Context, Template};
use crate::{
http::{Request, Response},
model::{user::Error as UserError, UserModel},
};
use async_trait::async_trait;

pub struct LoginController<T> {
redirect: Option<String>,
template: &'static str,
_marker: PhantomData<T>,
}

impl<T: UserModel> LoginController<T> {
pub fn new(template: &'static str) -> Self {
Self {
redirect: None,
template,
_marker: PhantomData,
}
}

pub fn redirect(mut self, redirect: impl ToString) -> Self {
self.redirect = Some(redirect.to_string());
self
}

fn error(&self, request: &Request, error: &str) -> Result<Response, Error> {
let template = Template::load(self.template)?;
let mut ctx = Context::new();
ctx.set(error, true)?;
ctx.set("request", request.clone())?;
Ok(Response::new().html(template.render(&ctx)?).code(400))
}
}

#[async_trait]
impl<T: UserModel> PageController for LoginController<T> {
async fn get(&self, request: &Request) -> Result<Response, Error> {
let mut ctx = Context::new();
ctx.set("request", request.clone())?;
let template = Template::load(self.template)?;
Ok(Response::new().html(template.render(&ctx)?))
}

async fn post(&self, request: &Request) -> Result<Response, Error> {
let form = request.form_data()?;
let identifier: String = match form.get_required("identifier") {
Ok(field) => field,
Err(_) => return self.error(request, "error_identifier"),
};
let identifier = identifier.trim().to_string();
let password: String = match form.get_required("password") {
Ok(field) => field,
Err(_) => return self.error(request, "error_password"),
};

match T::create_user(identifier, password).await {
Ok(user) => {
let id = user.id().integer()?;
let response = request.login(id);

if let Some(ref redirect) = self.redirect {
Ok(response.redirect(redirect))
} else {
Ok(response)
}
}
Err(err) => match err {
UserError::UserExists => return self.error(request, "error_user_exists"),
err => return Err(err.into()),
},
}
}
}

#[async_trait]
impl<T: UserModel> Controller for LoginController<T> {
async fn handle(&self, request: &Request) -> Result<Response, Error> {
PageController::handle(self, request).await
}
}
3 changes: 3 additions & 0 deletions rwf/src/model/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ pub enum Error {
"column \"{0}\" is missing from the row returned by the database,\ndid you forget to specify it in the query?"
)]
Column(String),

#[error("value is not an integer")]
NotAnInteger,
}

impl Error {
Expand Down
4 changes: 4 additions & 0 deletions rwf/src/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub mod prelude;
pub mod row;
pub mod select;
pub mod update;
pub mod user;
pub mod value;

pub use column::{Column, Columns, ToColumn};
Expand All @@ -48,6 +49,7 @@ pub use pool::{get_connection, get_pool, start_transaction, Connection, Connecti
pub use row::Row;
pub use select::Select;
pub use update::Update;
pub use user::UserModel;
pub use value::{ToValue, Value};

/// Convert a PostgreSQL row to a Rust struct. Type conversions are handled by `tokio_postgres`. This only
Expand Down Expand Up @@ -593,6 +595,8 @@ impl<T: Model> Query<T> {
}
}

/// If a unique constraint on any of these columns is triggered,
/// the row will be automatically updated.
pub fn unique_by(self, columns: &[impl ToColumn]) -> Self {
match self {
Query::Insert(insert) => Query::Insert(insert.unique_by(columns)),
Expand Down
8 changes: 8 additions & 0 deletions rwf/src/model/row.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ impl std::ops::Deref for Row {
}

impl Row {
/// Create new row.
pub fn new(row: tokio_postgres::Row) -> Self {
Self { row: Arc::new(row) }
}

/// Convert the row to a map of column names and values.
pub fn values(self) -> Result<HashMap<String, Value>, Error> {
let mut result = HashMap::new();
for column in self.columns() {
Expand All @@ -62,6 +64,12 @@ impl Row {

Ok(result)
}

/// Consume the row and return the inner `tokio_postgres::Row` if there
/// are no more references to this row.
pub fn into_inner(self) -> Option<tokio_postgres::Row> {
Arc::into_inner(self.row)
}
}

#[cfg(test)]
Expand Down
Loading

0 comments on commit b7359b3

Please sign in to comment.