diff --git a/backend/migrations/20241028023107_wishlist_add_received.down.sql b/backend/migrations/20241028023107_wishlist_add_received.down.sql new file mode 100644 index 0000000..6c52c6e --- /dev/null +++ b/backend/migrations/20241028023107_wishlist_add_received.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +alter table wishlist_items drop column received_at; diff --git a/backend/migrations/20241028023107_wishlist_add_received.up.sql b/backend/migrations/20241028023107_wishlist_add_received.up.sql new file mode 100644 index 0000000..948202b --- /dev/null +++ b/backend/migrations/20241028023107_wishlist_add_received.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +alter table wishlist_items add column received_at integer null; diff --git a/backend/src/main.rs b/backend/src/main.rs index 1218dca..0b56a91 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -11,12 +11,14 @@ mod google_oauth; mod middlewares; mod routes; mod user; +mod wishlist; use error_handling::AppError; use middlewares::inject_user_data; use google_oauth::{login, logout, google_auth_return}; -use routes::{about, contact, cottagecalendar, dashboard, index, profile, user_profile, user_wishlist, user_wishlist_add, user_wishlist_add_item, useradmin, wishlists}; +use routes::{about, contact, cottagecalendar, dashboard, index, profile, user_profile, useradmin}; use user::{add_user_role, delete_user_role, UserData}; +use wishlist::{user_wishlist, user_wishlist_add, user_wishlist_add_item, wishlists}; #[derive(Clone)] pub struct AppState { diff --git a/backend/src/routes.rs b/backend/src/routes.rs index a5e6d5c..2b9dcd0 100644 --- a/backend/src/routes.rs +++ b/backend/src/routes.rs @@ -2,16 +2,14 @@ use askama_axum::{Response, Template}; use axum::{ extract::{Path, State}, response::{Html, IntoResponse, Redirect}, - Extension, Form, + Extension, }; -use chrono::Utc; use http::StatusCode; -use serde::Deserialize; use sqlx::SqlitePool; use crate::{ middlewares::is_authorized, - user::{get_other_roles_display, get_user_roles_display, get_user_wishlist_items}, + user::{get_other_roles_display, get_user_roles_display}, UserData, }; @@ -251,155 +249,3 @@ pub async fn cottagecalendar( Redirect::to("/").into_response() } } - -#[derive(Template)] -#[template(path = "userwishlists.html")] -struct WishListsTemplate { - logged_in: bool, - name: String, - users: Vec, -} - -pub async fn wishlists( - Extension(user_data): Extension>, - State(db_pool): State, -) -> impl IntoResponse { - let user_name = user_data.as_ref().map(|s| s.name.clone()); - let logged_in = user_name.is_some(); - let name = user_name.unwrap_or_default(); - - let users = sqlx::query_as::<_, UserData>("SELECT * FROM users") - .fetch_all(&db_pool) - .await - .unwrap(); - - if is_authorized("/userwishlists", user_data, db_pool).await { - let template = WishListsTemplate { - logged_in, - name, - users, - }; - HtmlTemplate(template).into_response() - } else { - Redirect::to("/").into_response() - } -} - -#[derive(Template)] -#[template(path = "userwishlist.html")] -struct UserWishListTemplate { - logged_in: bool, - name: String, - user: UserData, - user_wishlist_items: Vec, -} - -pub async fn user_wishlist( - Path(user_id): Path, - State(db_pool): State, - Extension(user_data): Extension>, -) -> impl IntoResponse { - // Extract the user's name from the user data. - let user_name = user_data.as_ref().map(|s| s.name.clone()); - let logged_in = user_data.is_some(); - let name = user_name.unwrap_or_default(); - - // Extract the user data. - let user = sqlx::query_as!(UserData, "SELECT * FROM users WHERE id = ?", user_id) - .fetch_one(&db_pool) - .await - .unwrap(); - - if is_authorized("/wishlist", user_data, db_pool.clone()).await { - // Get user roles - let user_wishlist_items = get_user_wishlist_items(user_id, &db_pool.clone()).await; - - // Create the wishlist template. - let template = UserWishListTemplate { - logged_in, - name, - user: user, - user_wishlist_items, - }; - return HtmlTemplate(template).into_response(); - } else { - Redirect::to("/").into_response() - } -} - -#[derive(Template)] -#[template(path = "userwishlistadd.html")] -struct UserWishListAddTemplate { - logged_in: bool, - name: String, - user: UserData, - user_wishlist_items: Vec, -} - -pub async fn user_wishlist_add( - Path(user_id): Path, - State(db_pool): State, - Extension(user_data): Extension>, -) -> impl IntoResponse { - // Extract the user's name from the user data. - let user_name = user_data.as_ref().map(|s| s.name.clone()); - let logged_in = user_data.is_some(); - let name = user_name.unwrap_or_default(); - - // Extract the user data. - let user = sqlx::query_as!(UserData, "SELECT * FROM users WHERE id = ?", user_id) - .fetch_one(&db_pool) - .await - .unwrap(); - - if is_authorized("/wishlist", user_data, db_pool.clone()).await { - // Get user roles - let user_wishlist_items = get_user_wishlist_items(user_id, &db_pool.clone()).await; - - // Create the wishlist template. - let template = UserWishListAddTemplate { - logged_in, - name, - user: user, - user_wishlist_items, - }; - return HtmlTemplate(template).into_response(); - } else { - Redirect::to("/").into_response() - } -} - -#[derive(Deserialize, Debug)] -pub struct ItemForm { - item: String, - item_url: String, -} - -pub async fn user_wishlist_add_item( - Path(user_id): Path, - State(db_pool): State, - Extension(user_data): Extension>, - Form(item_form): Form -) -> impl IntoResponse { - if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await { - // Insert new item to database - let now = Utc::now().timestamp(); - - sqlx::query("insert into wishlist_items (created_at, created_by, updated_at, updated_by, user_id, item, item_url) values (?, ?, ?, ?, ?, ?, ?)") - .bind(now)// Created now - .bind(user_data.as_ref().unwrap().id)// Created by current user - .bind(now) // Updated now - .bind(user_data.as_ref().unwrap().id) // Updated by current user - .bind(user_id) - .bind(item_form.item) - .bind(item_form.item_url) - .execute(&db_pool) - .await - .unwrap(); - - let redirect_string = format!("/userwishlist/{user_id}"); - Redirect::to(&redirect_string).into_response() - } else { - Redirect::to("/").into_response() - } -} diff --git a/backend/src/user.rs b/backend/src/user.rs index b3e5634..37581d8 100644 --- a/backend/src/user.rs +++ b/backend/src/user.rs @@ -5,7 +5,7 @@ use axum::{ Extension, }; use chrono::Utc; -use reqwest::redirect; + ///User related structs and functions use serde::{Deserialize, Serialize}; use sqlx::{prelude::FromRow, SqlitePool}; diff --git a/backend/src/wishlist.rs b/backend/src/wishlist.rs new file mode 100644 index 0000000..fd9a685 --- /dev/null +++ b/backend/src/wishlist.rs @@ -0,0 +1,188 @@ +use askama_axum::{IntoResponse, Response, Template}; +use axum::{extract::{Path, State}, response::Redirect, Extension, Form}; +use axum_extra::response::Html; +use chrono::Utc; +use http::StatusCode; +use serde::Deserialize; +use sqlx::SqlitePool; + +use crate::{middlewares::is_authorized, user::{get_user_wishlist_items, UserData}}; + +struct HtmlTemplate(T); + +impl IntoResponse for HtmlTemplate +where + T: Template, +{ + fn into_response(self) -> Response { + match self.0.render() { + Ok(html) => Html(html).into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to render template. Error: {}", err), + ) + .into_response(), + } + } +} + +#[derive(Template)] +#[template(path = "userwishlists.html")] +struct WishListsTemplate { + logged_in: bool, + name: String, + users: Vec, +} + +pub async fn wishlists( + Extension(user_data): Extension>, + State(db_pool): State, +) -> impl IntoResponse { + let user_name = user_data.as_ref().map(|s| s.name.clone()); + let logged_in = user_name.is_some(); + let name = user_name.unwrap_or_default(); + + let users = sqlx::query_as::<_, UserData>("SELECT * FROM users") + .fetch_all(&db_pool) + .await + .unwrap(); + + if is_authorized("/userwishlists", user_data, db_pool).await { + let template = WishListsTemplate { + logged_in, + name, + users, + }; + HtmlTemplate(template).into_response() + } else { + Redirect::to("/").into_response() + } +} + +#[derive(Template)] +#[template(path = "userwishlist.html")] +struct UserWishListTemplate { + logged_in: bool, + name: String, + my_wishlist: bool, + user: UserData, + user_wishlist_items: Vec, +} + +pub async fn user_wishlist( + Path(user_id): Path, + State(db_pool): State, + Extension(user_data): Extension>, +) -> impl IntoResponse { + // Extract the user's name from the user data. + let user_name = user_data.as_ref().map(|s| s.name.clone()); + let logged_in = user_data.is_some(); + let name = user_name.unwrap_or_default(); + + // Extract the user's id from the user data + let user_userid = user_data.as_ref().map(|s| s.id.clone()); + let userid = user_userid.unwrap_or_default(); + + // Extract the user data. + let user = sqlx::query_as!(UserData, "SELECT * FROM users WHERE id = ?", user_id) + .fetch_one(&db_pool) + .await + .unwrap(); + + if is_authorized("/wishlist", user_data, db_pool.clone()).await { + // Get user wishlist + let user_wishlist_items = get_user_wishlist_items(user_id, &db_pool.clone()).await; + + // Is viewed and viewing user the same (my wishlist)? + let my_wishlist = user_id == userid; + + // Create the wishlist template. + let template = UserWishListTemplate { + logged_in, + name, + my_wishlist, + user: user, + user_wishlist_items, + }; + return HtmlTemplate(template).into_response(); + } else { + Redirect::to("/").into_response() + } +} + +#[derive(Template)] +#[template(path = "userwishlistadd.html")] +struct UserWishListAddTemplate { + logged_in: bool, + name: String, + user: UserData, + user_wishlist_items: Vec, +} + +pub async fn user_wishlist_add( + Path(user_id): Path, + State(db_pool): State, + Extension(user_data): Extension>, +) -> impl IntoResponse { + // Extract the user's name from the user data. + let user_name = user_data.as_ref().map(|s| s.name.clone()); + let logged_in = user_data.is_some(); + let name = user_name.unwrap_or_default(); + + // Extract the user data. + let user = sqlx::query_as!(UserData, "SELECT * FROM users WHERE id = ?", user_id) + .fetch_one(&db_pool) + .await + .unwrap(); + + if is_authorized("/wishlist", user_data, db_pool.clone()).await { + // Get user roles + let user_wishlist_items = get_user_wishlist_items(user_id, &db_pool.clone()).await; + + // Create the wishlist template. + let template = UserWishListAddTemplate { + logged_in, + name, + user: user, + user_wishlist_items, + }; + return HtmlTemplate(template).into_response(); + } else { + Redirect::to("/").into_response() + } +} + +#[derive(Deserialize, Debug)] +pub struct ItemForm { + item: String, + item_url: String, +} + +pub async fn user_wishlist_add_item( + Path(user_id): Path, + State(db_pool): State, + Extension(user_data): Extension>, + Form(item_form): Form +) -> impl IntoResponse { + if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await { + // Insert new item to database + let now = Utc::now().timestamp(); + + sqlx::query("insert into wishlist_items (created_at, created_by, updated_at, updated_by, user_id, item, item_url) values (?, ?, ?, ?, ?, ?, ?)") + .bind(now)// Created now + .bind(user_data.as_ref().unwrap().id)// Created by current user + .bind(now) // Updated now + .bind(user_data.as_ref().unwrap().id) // Updated by current user + .bind(user_id) + .bind(item_form.item) + .bind(item_form.item_url) + .execute(&db_pool) + .await + .unwrap(); + + let redirect_string = format!("/userwishlist/{user_id}"); + Redirect::to(&redirect_string).into_response() + } else { + Redirect::to("/").into_response() + } +} diff --git a/backend/templates/userwishlist.html b/backend/templates/userwishlist.html index adb6852..e4668d5 100644 --- a/backend/templates/userwishlist.html +++ b/backend/templates/userwishlist.html @@ -1,20 +1,32 @@ {% extends "authorized.html" %} {% block title %}User Profile{% endblock %} {% block center %} +{% if my_wishlist %} +

My Wishlist

+{% else %}

{{ user.given_name }} Wishlist

+{% endif %}

List

+{% if my_wishlist %} Add +{% endif %} + {% for user_wishlist_item in user_wishlist_items %} + {% if my_wishlist %} + + {% else %} + + {% endif %} {% endfor %}
ItemAction
{{ user_wishlist_item.item }}ReceivedBought