diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6f90e20..fbadcd5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,3 +5,7 @@ FROM mcr.microsoft.com/devcontainers/rust:1-1-bookworm RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install clang lld \ && apt-get autoremove -y && apt-get clean -y + +RUN curl -fsSL https://ollama.com/install.sh | sh + +RUN cargo instal sqlx-cli diff --git a/backend/migrations/20241025205504_wishlist.down.sql b/backend/migrations/20241025205504_wishlist.down.sql new file mode 100644 index 0000000..0aa61b2 --- /dev/null +++ b/backend/migrations/20241025205504_wishlist.down.sql @@ -0,0 +1,4 @@ +-- Add down migration script here +drop table if exists `wishlist_items`; + +delete from `role_permissions` where id = 9; \ No newline at end of file diff --git a/backend/migrations/20241025205504_wishlist.up.sql b/backend/migrations/20241025205504_wishlist.up.sql new file mode 100644 index 0000000..44e7b6a --- /dev/null +++ b/backend/migrations/20241025205504_wishlist.up.sql @@ -0,0 +1,16 @@ +-- Add up migration script here +CREATE TABLE + `wishlist_items` ( + `id` integer not null primary key autoincrement, + `created_at` INTEGER not null default CURRENT_TIMESTAMP, + `created_by` ineger null, + `updated_at` INTEGER null default CURRENT_TIMESTAMP, + `updated_by` integer null, + `user_id` INTEGER null, + `item` varchar(255) null, + `item_url` varchar(255) null, + `purchased_by` INTEGER null, + unique (`id`) + ); + +insert into `role_permissions` (`created_at`, `created_by`, `id`, `item`, `role_id`, `updated_at`, `updated_by`) values ('0', '0', '9', '/wishlist', '2', '0', '0') \ No newline at end of file diff --git a/backend/src/google_oauth.rs b/backend/src/google_oauth.rs index 7062783..0c690dd 100644 --- a/backend/src/google_oauth.rs +++ b/backend/src/google_oauth.rs @@ -244,7 +244,7 @@ pub async fn google_auth_return( .await?; } } - Ok((headers, Redirect::to("/"))) + Ok((headers, Redirect::to("/dashboard"))) } pub async fn logout( diff --git a/backend/src/main.rs b/backend/src/main.rs index 32b9f93..1218dca 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -15,7 +15,7 @@ mod user; use error_handling::AppError; use middlewares::inject_user_data; use google_oauth::{login, logout, google_auth_return}; -use routes::{dashboard, index, about, cottagecalendar, contact, profile, user_profile, useradmin}; +use routes::{about, contact, cottagecalendar, dashboard, index, profile, user_profile, user_wishlist, user_wishlist_add, user_wishlist_add_item, useradmin, wishlists}; use user::{add_user_role, delete_user_role, UserData}; #[derive(Clone)] @@ -53,6 +53,9 @@ async fn main() { .route("/users/:user_id", get(user_profile)) .route("/roles/:user_id/:role_id/add", get(add_user_role)) .route("/roles/:user_role_id/delete", get(delete_user_role)) + .route("/wishlists", get(wishlists)) + .route("/userwishlist/:user_id", get(user_wishlist)) + .route("/userwishlist/add/:user_id", get(user_wishlist_add).post(user_wishlist_add_item)) .nest_service("/assets", ServeDir::new("templates/assets") .fallback(get_service(ServeDir::new("templates/assets")))) .route("/", get(index)) diff --git a/backend/src/routes.rs b/backend/src/routes.rs index 2b9dcd0..a5e6d5c 100644 --- a/backend/src/routes.rs +++ b/backend/src/routes.rs @@ -2,14 +2,16 @@ use askama_axum::{Response, Template}; use axum::{ extract::{Path, State}, response::{Html, IntoResponse, Redirect}, - Extension, + Extension, Form, }; +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}, + user::{get_other_roles_display, get_user_roles_display, get_user_wishlist_items}, UserData, }; @@ -249,3 +251,155 @@ 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 3dbbea9..b3e5634 100644 --- a/backend/src/user.rs +++ b/backend/src/user.rs @@ -5,6 +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}; @@ -58,6 +59,20 @@ pub struct UserRolesDisplay { pub role_id: i64, pub role_name: String, } + +#[derive(Default, Clone, Debug, FromRow, Serialize, Deserialize)] +pub struct UserWishlistItem { + pub id: i64, + pub created_at: i64, + pub created_by: i64, + pub updated_at: i64, + pub updated_by: i64, + pub user_id: i64, + pub item: String, + pub item_url: String, + pub purchased_by: i64, +} + /* pub async fn get_user_roles(user_id: i64, db_pool: &SqlitePool) -> Vec { // Get user roles @@ -107,11 +122,9 @@ pub async fn add_user_role( if is_authorized("/roles", user_data.clone(), db_pool.clone()).await { let now = Utc::now().timestamp(); - println!("Adding role {} to user {}", role_id, user_id); - sqlx::query("INSERT INTO user_roles (created_at, created_by, updated_at, updated_by, user_id, role_id) VALUES (?, ?, ?, ?, ?, ?)") - .bind(now)// Created now - .bind(user_data.as_ref().unwrap().id)// Created by current user + .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) @@ -119,8 +132,12 @@ pub async fn add_user_role( .execute(&db_pool) .await .unwrap(); + + let redirect_url = format!("/users/{user_id}"); + Redirect::to(&redirect_url).into_response() + } else { + Redirect::to("/").into_response() } - Redirect::to("/").into_response() } pub async fn delete_user_role( @@ -137,3 +154,16 @@ pub async fn delete_user_role( } Redirect::to("/").into_response() } + +pub async fn get_user_wishlist_items(user_id: i64, db_pool: &SqlitePool) -> Vec { + // Get wish list items for the user + let user_wishlist_items = sqlx::query_as( + r#"select id, created_at, created_by, updated_at, updated_by, user_id, item, item_url, purchased_by from wishlist_items where user_id = ?"# + ) + .bind(user_id) + .fetch_all(db_pool) + .await + .unwrap(); + + user_wishlist_items +} \ No newline at end of file diff --git a/backend/templates/authorized.html b/backend/templates/authorized.html index c154e87..d80efc1 100644 --- a/backend/templates/authorized.html +++ b/backend/templates/authorized.html @@ -9,6 +9,7 @@
  • Web links
  • User Administration
  • Cottage Calendar
  • +
  • Wish lists
  • diff --git a/backend/templates/userwishlist.html b/backend/templates/userwishlist.html new file mode 100644 index 0000000..adb6852 --- /dev/null +++ b/backend/templates/userwishlist.html @@ -0,0 +1,22 @@ +{% extends "authorized.html" %} +{% block title %}User Profile{% endblock %} +{% block center %} +

    {{ user.given_name }} Wishlist

    +
    +

    List

    +Add + + + + + + + + {% for user_wishlist_item in user_wishlist_items %} + + + + {% endfor %} + +
    Item
    {{ user_wishlist_item.item }}
    +{% endblock center %} diff --git a/backend/templates/userwishlistadd.html b/backend/templates/userwishlistadd.html new file mode 100644 index 0000000..0f2cee8 --- /dev/null +++ b/backend/templates/userwishlistadd.html @@ -0,0 +1,17 @@ +{% extends "authorized.html" %} +{% block title %}Add to {{ user.given_name }} Wishlist{% endblock %} +{% block center %} +

    Add Item to Wishlist

    +
    +
    + + +
    +
    + + +
    + +
    +
    +{% endblock center %} diff --git a/backend/templates/userwishlists.html b/backend/templates/userwishlists.html new file mode 100644 index 0000000..1ceca1a --- /dev/null +++ b/backend/templates/userwishlists.html @@ -0,0 +1,23 @@ +{% extends "authorized.html" %} +{% block title %}Wish Lists{% endblock %} +{% block center %} +

    Users

    + + + + + + + + + + {% for user in users %} + + + + + + {% endfor %} + +
    IDNameemail
    {{ user.id }}{{ user.name }}{{ user.email }}
    +{% endblock center %} \ No newline at end of file