diff --git a/.gitignore b/.gitignore index 474d7e9..c760787 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ backend/target backend/db backend/id_rsa backend/id_rsa.pub +backend/sqlite3.env +backend/.env diff --git a/backend/.env b/backend/.env deleted file mode 100644 index e04935b..0000000 --- a/backend/.env +++ /dev/null @@ -1,7 +0,0 @@ -DATABASE_URL=sqlite://db/db.sqlite3 -GOOGLE_CLIENT_ID=735264084619-clsmvgdqdmum4rvrcj0kuk28k9agir1c.apps.googleusercontent.com -GOOGLE_CLIENT_SECRET=L6uI7FQGoMJd-ay1HO_iGJ6M -SMTP_SERVER_NAME=mailout.easymail.ca -SMTP_SERVER_PORT=587 -EMAIL_USERNAME=admin@jean-marie.ca -EMAIL_PASSWORD=Cj6wX8^JivPD diff --git a/backend/Cargo.lock b/backend/Cargo.lock index a2fa63b..858f744 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -2603,6 +2603,7 @@ dependencies = [ "atoi", "byteorder", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -2631,6 +2632,7 @@ dependencies = [ "tokio-stream", "tracing", "url", + "uuid", ] [[package]] @@ -2683,6 +2685,7 @@ dependencies = [ "bitflags 2.6.0", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -2711,6 +2714,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -2724,6 +2728,7 @@ dependencies = [ "base64 0.22.1", "bitflags 2.6.0", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -2749,6 +2754,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -2759,6 +2765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -2773,6 +2780,7 @@ dependencies = [ "sqlx-core", "tracing", "url", + "uuid", ] [[package]] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 5da6e90..b197ee1 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -23,7 +23,7 @@ oauth2 = "4.4" http = "1.1" tower-http = { version = "0.6.1", features = ["full"] } chrono = { version = "0.4.38", features = ["serde"] } -sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "macros"] } +sqlx = { version = "0.8", features = ["postgres", "sqlite","runtime-tokio", "macros", "chrono", "uuid"] } uuid = { version = "1.10", features = ["v4"] } dotenvy = "0.15" constant_time_eq = "0.3" diff --git a/backend/migrations/20241006175727_initial_setup.down.sql b/backend/migrations/20241006175727_initial_setup.down.sql deleted file mode 100644 index d2f607c..0000000 --- a/backend/migrations/20241006175727_initial_setup.down.sql +++ /dev/null @@ -1 +0,0 @@ --- Add down migration script here diff --git a/backend/migrations/20241006175727_initial_setup.up.sql b/backend/migrations/20241006175727_initial_setup.up.sql deleted file mode 100644 index 87d36ea..0000000 --- a/backend/migrations/20241006175727_initial_setup.up.sql +++ /dev/null @@ -1,58 +0,0 @@ --- Add up migration script here -CREATE TABLE "oauth2_state_storage" ( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "csrf_state" text NOT NULL, - "pkce_code_verifier" text NOT NULL, - "return_url" text NOT NULL -); - -CREATE TABLE "user_sessions" ( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "user_id" integer NOT NULL, - "session_token_p1" text NOT NULL, - "session_token_p2" text NOT NULL, - "created_at" integer NOT NULL, - "expires_at" integer NOT NULL -); - -CREATE TABLE "users" ( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "created_at" integer NOT NULL, - "created_by" integer NOT NULL, - "updated_at" integer NOT NULL, - "updated_by" integer NOT NULL, - "email" text NOT NULL UNIQUE, - "name" text NOT NULL, - "family_name" text NOT NULL, - "given_name" text NOT NULL -); - -CREATE TABLE IF NOT EXISTS roles ( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "created_at" integer NOT NULL, - "created_by" integer NOT NULL, - "updated_at" integer NOT NULL, - "updated_by" integer NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT -); - -CREATE TABLE IF NOT EXISTS user_roles ( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "created_at" integer NOT NULL, - "created_by" integer NOT NULL, - "updated_at" integer NOT NULL, - "updated_by" integer NOT NULL, - "user_id" integer NOT NULL, - "role_id" integer NOT NULL -); - -create TABLE IF NOT EXISTS role_permissions ( - "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, - "created_at" integer NOT NULL, - "created_by" integer NOT NULL, - "updated_at" integer NOT NULL, - "updated_by" integer NOT NULL, - "role_id" integer NOT NULL, - "item" text NOT NULL -); diff --git a/backend/migrations/20241006211932_initial_roles.down.sql b/backend/migrations/20241006211932_initial_roles.down.sql deleted file mode 100644 index d2f607c..0000000 --- a/backend/migrations/20241006211932_initial_roles.down.sql +++ /dev/null @@ -1 +0,0 @@ --- Add down migration script here diff --git a/backend/migrations/20241006211932_initial_roles.up.sql b/backend/migrations/20241006211932_initial_roles.up.sql deleted file mode 100644 index 3273d6d..0000000 --- a/backend/migrations/20241006211932_initial_roles.up.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Add up migration script here -INSERT INTO "main"."roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "name", "description") VALUES ('1', '0', '0', '0', '0', 'public', 'Users with only anonymous access'); -INSERT INTO "main"."roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "name", "description") VALUES ('2', '0', '0', '0', '0', 'normal', 'Users with no elevated privileges'); -INSERT INTO "main"."roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "name", "description") VALUES ('3', '0', '0', '0', '0', 'editor', 'Users with basic elevated privileges'); -INSERT INTO "main"."roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "name", "description") VALUES ('4', '0', '0', '0', '0', 'admin', 'Users with full administrative privileges'); - diff --git a/backend/migrations/20241007024816_initial_data.down.sql b/backend/migrations/20241007024816_initial_data.down.sql deleted file mode 100644 index d2f607c..0000000 --- a/backend/migrations/20241007024816_initial_data.down.sql +++ /dev/null @@ -1 +0,0 @@ --- Add down migration script here diff --git a/backend/migrations/20241007024816_initial_data.up.sql b/backend/migrations/20241007024816_initial_data.up.sql deleted file mode 100644 index 9632bfc..0000000 --- a/backend/migrations/20241007024816_initial_data.up.sql +++ /dev/null @@ -1,12 +0,0 @@ --- Add up migration script here --- Role permissions -INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('1', '0', '0', '0', '0', '1', '/'); -INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('2', '0', '0', '0', '0', '1', '/login'); -INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('3', '0', '0', '0', '0', '1', '/logout'); -INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('4', '0', '0', '0', '0', '2', '/dashboard'); -INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('5', '0', '0', '0', '0', '2', '/profile'); -INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('6', '0', '0', '0', '0', '4', '/useradmin'); -INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('7', '0', '0', '0', '0', '4', '/users'); - --- First user is an admin -INSERT INTO "main"."user_roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "user_id", "role_id") VALUES ('2', '0', '0', '0', '0', '1', '4'); diff --git a/backend/migrations/20241018235116_calendar_roles.down.sql b/backend/migrations/20241018235116_calendar_roles.down.sql deleted file mode 100644 index 5910f34..0000000 --- a/backend/migrations/20241018235116_calendar_roles.down.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Add down migration script here --- Delete role records -DELETE FROM "main"."roles" WHERE "id" = '5'; - --- Delete permission records -DELETE FROM "main"."role_permissions" WHERE "id" = '8'; - --- Delete user role records -DELETE FROM "main"."user_roles" WHERE "role_id" = '5'; diff --git a/backend/migrations/20241018235116_calendar_roles.up.sql b/backend/migrations/20241018235116_calendar_roles.up.sql deleted file mode 100644 index 4f7c40b..0000000 --- a/backend/migrations/20241018235116_calendar_roles.up.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Add up migration script here --- Add roles for calendar -INSERT INTO "main"."roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "name", "description") VALUES ('5', '0', '0', '0', '0', 'calendar', 'Users with access to the calendar'); - --- Add permissions for calendar -INSERT INTO "main"."role_permissions" ("id", "created_at", "created_by", "updated_at", "updated_by", "role_id", "item") VALUES ('8', '0', '0', '0', '0', '5', '/cottagecalendar'); - --- Add user roles for calendar -INSERT INTO "main"."user_roles" ("id", "created_at", "created_by", "updated_at", "updated_by", "user_id", "role_id") VALUES ('1', '0', '0', '0', '0', '1', '5'); diff --git a/backend/migrations/20241025205504_wishlist.down.sql b/backend/migrations/20241025205504_wishlist.down.sql deleted file mode 100644 index 0aa61b2..0000000 --- a/backend/migrations/20241025205504_wishlist.down.sql +++ /dev/null @@ -1,4 +0,0 @@ --- 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 deleted file mode 100644 index 44e7b6a..0000000 --- a/backend/migrations/20241025205504_wishlist.up.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 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/migrations/20241028023107_wishlist_add_received.down.sql b/backend/migrations/20241028023107_wishlist_add_received.down.sql deleted file mode 100644 index 6c52c6e..0000000 --- a/backend/migrations/20241028023107_wishlist_add_received.down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- 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 deleted file mode 100644 index 948202b..0000000 --- a/backend/migrations/20241028023107_wishlist_add_received.up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Add up migration script here -alter table wishlist_items add column received_at integer null; diff --git a/backend/migrations/20241103140734_secret-gift-exchange.down.sql b/backend/migrations/20241103140734_secret-gift-exchange.down.sql deleted file mode 100644 index 506c33d..0000000 --- a/backend/migrations/20241103140734_secret-gift-exchange.down.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Add down migration script here -drop table gift_exchange; -drop table gift_exchange_participants; - -delete from role_permissions where item = '/giftexchange'; diff --git a/backend/migrations/20241103140734_secret-gift-exchange.up.sql b/backend/migrations/20241103140734_secret-gift-exchange.up.sql deleted file mode 100644 index 0d5946a..0000000 --- a/backend/migrations/20241103140734_secret-gift-exchange.up.sql +++ /dev/null @@ -1,28 +0,0 @@ --- Add up migration script here -CREATE TABLE - `gift_exchange` ( - `id` integer not null primary key autoincrement, - `created_at` INTEGER not null default CURRENT_TIMESTAMP, - `created_by` integer not null default 0, - `updated_at` INTEGER not null default CURRENT_TIMESTAMP, - `updated_by` integer not null default 0, - `name` varchar(255) not null, - `exchange_date` INTEGER not null, - `status` INTEGER not null default 0, - unique (`id`) - ); - - CREATE TABLE - `gift_exchange_participants` ( - `id` integer not null primary key autoincrement, - `created_at` INTEGER not null default CURRENT_TIMESTAMP, - `created_by` integer not null default 0, - `updated_at` INTEGER not null default CURRENT_TIMESTAMP, - `updated_by` integer not null default 0, - `exchange_id` INTEGER not null, - `participant_id` INTEGER not null, - `gifter_id` INTEGER not null, - unique (`id`) - ); - -insert into `role_permissions` (`created_at`, `created_by`, `id`, `item`, `role_id`, `updated_at`, `updated_by`) values ('0', '0', '10', '/giftexchange', '2', '0', '0') \ No newline at end of file diff --git a/backend/migrations/20241113203107_calendar_tables.down.sql b/backend/migrations/20241113203107_calendar_tables.down.sql deleted file mode 100644 index 42af142..0000000 --- a/backend/migrations/20241113203107_calendar_tables.down.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Add down migration script here - -drop table if exists `calendar_events`; -drop table if exists `calendar_event_types`; -drop table if exists `calendar`; diff --git a/backend/migrations/20241113203107_calendar_tables.up.sql b/backend/migrations/20241113203107_calendar_tables.up.sql deleted file mode 100644 index 66691e8..0000000 --- a/backend/migrations/20241113203107_calendar_tables.up.sql +++ /dev/null @@ -1,42 +0,0 @@ --- Add up migration script here - --- Calendars --- 1 - Cottage --- 2 - Family tree -create table calendar ( - id integer not null primary key autoincrement, - created_at integer not null default CURRENT_TIMESTAMP, - created_by integer not null default 0, - updated_at integer null default CURRENT_TIMESTAMP, - updated_by integer not null default 0, - name varchar(255) not null -); - --- Event types --- 1 - Rental --- 2 - Life event -create table calendar_event_types ( - id integer not null primary key autoincrement, - created_at integer not null default CURRENT_TIMESTAMP, - created_by integer not null default 0, - updated_at integer null default CURRENT_TIMESTAMP, - updated_by integer not null default 0, - name varchar(255) not null -); - -create table calendar_events ( - id integer not null primary key autoincrement, - created_at integer not null default CURRENT_TIMESTAMP, - created_by integer not null default 0, - updated_at integer null default CURRENT_TIMESTAMP, - updated_by integer not null default 0, - calendar_id integer not null, - event_type_id integer not null, - title varchar(255) not null, - description varchar(255) null, - start_time integer null, - end_time integer null, - repeat_type integer not null default 0, -- 0 - None, 1 - Daily, 2 - Weekly, 3 - Monthly, 4 - Yearly, 5 - Day of week, 6 - Day of month - repeat_interval integer not null default 0, - celebrate boolean not null default 0 -); diff --git a/backend/migrations/20241210153133_inital_tables.down.sql b/backend/migrations/20241210153133_inital_tables.down.sql new file mode 100644 index 0000000..4418e95 --- /dev/null +++ b/backend/migrations/20241210153133_inital_tables.down.sql @@ -0,0 +1,13 @@ +-- Drop Postgres tables +drop table if exists oauth2_state_storage; +drop table if exists user_sessions; +drop table if exists users; +drop table if exists roles; +drop table if exists user_roles; +drop table if exists role_permissions; +drop table if exists wishlist_items; +drop table if exists gift_exchange; +drop table if exists gift_exchange_participants; +drop table if exists calendar; +drop table if exists calendar_event_types; +drop table if exists calendar_events; diff --git a/backend/migrations/20241210153133_inital_tables.up.sql b/backend/migrations/20241210153133_inital_tables.up.sql new file mode 100644 index 0000000..726e5ac --- /dev/null +++ b/backend/migrations/20241210153133_inital_tables.up.sql @@ -0,0 +1,386 @@ +-- Create Postgres tables +create table if not exists oauth2_state_storage ( + id uuid PRIMARY KEY default gen_random_uuid(), + csrf_state text NOT NULL, + pkce_code_verifier text NOT NULL, + return_url text NOT NULL +); + +create table if not exists user_sessions ( + id uuid PRIMARY KEY default gen_random_uuid(), + user_id uuid NOT NULL, + session_token_p1 text NOT NULL, + session_token_p2 text NOT NULL, + created_at timestamp NOT NULL, + expires_at timestamp NOT NULL +); + +create table if not exists users ( + id uuid PRIMARY KEY default gen_random_uuid(), + created_at timestamp NOT NULL default now(), + created_by uuid NOT NULL, + updated_at timestamp NOT NULL default now(), + updated_by uuid NOT NULL, + email text NOT NULL UNIQUE, + "name" text NOT NULL, + family_name text NOT NULL, + given_name text NOT NULL +); + +create table IF NOT EXISTS roles ( + id uuid PRIMARY KEY default gen_random_uuid(), + created_at timestamp NOT NULL default now(), + created_by uuid NOT NULL, + updated_at timestamp NOT NULL default now(), + updated_by uuid NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT +); + +create table IF NOT EXISTS user_roles ( + id uuid PRIMARY KEY default gen_random_uuid(), + created_at timestamp NOT NULL default now(), + created_by uuid NOT NULL, + updated_at timestamp NOT NULL default now(), + updated_by uuid NOT NULL, + user_id uuid NOT NULL, + role_id uuid NOT NULL +); + +create unique index if not exists unique_user_role on user_roles(user_id, role_id); + +create table IF NOT EXISTS role_permissions ( + id uuid PRIMARY KEY default gen_random_uuid(), + created_at timestamp NOT NULL default now(), + created_by uuid NOT NULL, + updated_at timestamp NOT NULL default now(), + updated_by uuid NOT NULL, + role_id uuid NOT NULL, + item text NOT NULL +); + +create table if not exists wishlist_items ( + id uuid PRIMARY KEY default gen_random_uuid(), + created_at timestamp not null default now(), + created_by uuid null, + updated_at timestamp null default now(), + updated_by uuid null, + user_id uuid null, + item varchar(512) null, + item_url varchar(1024) null, + purchased_by uuid null, + received_at timestamp null +); + +create table if not exists gift_exchange ( + id uuid PRIMARY KEY default gen_random_uuid(), + created_at timestamp not null default now(), + created_by uuid not null, + updated_at timestamp not null default now(), + updated_by uuid not null, + "name" varchar(255) not null, + exchange_date timestamp not null, + "status" INTEGER not null default 0 +); + +create table if not exists gift_exchange_participants ( + id uuid PRIMARY KEY default gen_random_uuid(), + created_at timestamp not null default now(), + created_by uuid not null, + updated_at timestamp not null default now(), + updated_by uuid not null, + exchange_id uuid not null, + participant_id uuid not null, + gifter_id uuid not null +); + +-- Calendars +-- 1 - Cottage +-- 2 - Family tree +create table if not exists calendar ( + id uuid PRIMARY KEY default gen_random_uuid(), + created_at timestamp not null default now(), + created_by uuid not null, + updated_at timestamp null default now(), + updated_by uuid not null, + "name" varchar(255) not null +); + +-- Event types +-- 1 - Rental +-- 2 - Life event +create table if not exists calendar_event_types ( + id uuid PRIMARY KEY default gen_random_uuid(), + created_at timestamp not null default now(), + created_by uuid not null, + updated_at timestamp null default now(), + updated_by uuid not null, + "name" varchar(255) not null +); + +create table if not exists calendar_events ( + id uuid PRIMARY KEY default gen_random_uuid(), + created_at timestamp not null default now(), + created_by uuid not null, + updated_at timestamp null default now(), + updated_by uuid not null, + calendar_id uuid not null, + event_type_id uuid not null, + title varchar(255) not null, + "description" varchar(255) null, + start_time timestamp null, + end_time timestamp null, + repeat_type integer not null default 0, + -- 0 - None, 1 - Daily, 2 - Weekly, 3 - Monthly, 4 - Yearly, 5 - Day of week, 6 - Day of month + repeat_interval integer not null default 0, + celebrate boolean not null default true +); + +do $$ + +declare user_uuid uuid := gen_random_uuid(); + +begin -- Initial user +insert into users ( + id, + "name", + created_by, + updated_by, + email, + family_name, + given_name + ) +values + ( + user_uuid, + 'admin', + user_uuid, + user_uuid, + 'admin@jean-marie.ca', + '', + 'admin' + ); + +-- Initial roles +INSERT INTO + roles (created_by, updated_by, "name", "description") +VALUES + ( + user_uuid, + user_uuid, + 'public', + 'Users with only anonymous access' + ); + +INSERT INTO + roles (created_by, updated_by, "name", "description") +VALUES + ( + user_uuid, + user_uuid, + 'normal', + 'Users with no elevated privileges' + ); + +INSERT INTO + roles (created_by, updated_by, "name", "description") +VALUES + ( + user_uuid, + user_uuid, + 'editor', + 'Users with basic elevated privileges' + ); + +INSERT INTO + roles (created_by, updated_by, "name", "description") +VALUES + ( + user_uuid, + user_uuid, + 'admin', + 'Users with full administrative privileges' + ); + +INSERT INTO + roles (created_by, updated_by, "name", "description") +VALUES + ( + user_uuid, + user_uuid, + 'calendar', + 'Users with access to the calendar' + ); + +-- Initial permissions +INSERT INTO + role_permissions (created_by, updated_by, role_id, item) +VALUES + ( + user_uuid, + user_uuid, + ( + SELECT + id + FROM + roles + WHERE + "name" = 'public' + ), + '/' + ); + +INSERT INTO + role_permissions (created_by, updated_by, role_id, item) +VALUES + ( + user_uuid, + user_uuid, + ( + SELECT + id + FROM + roles + WHERE + "name" = 'public' + ), + '/login' + ); + +INSERT INTO + role_permissions (created_by, updated_by, role_id, item) +VALUES + ( + user_uuid, + user_uuid, + ( + SELECT + id + FROM + roles + WHERE + "name" = 'public' + ), + '/logout' + ); + +INSERT INTO + role_permissions (created_by, updated_by, role_id, item) +VALUES + ( + user_uuid, + user_uuid, + ( + SELECT + id + FROM + roles + WHERE + "name" = 'normal' + ), + '/dashboard' + ); + +INSERT INTO + role_permissions (created_by, updated_by, role_id, item) +VALUES + ( + user_uuid, + user_uuid, + ( + SELECT + id + FROM + roles + WHERE + "name" = 'normal' + ), + '/profile' + ); + +INSERT INTO + role_permissions (created_by, updated_by, role_id, item) +VALUES + ( + user_uuid, + user_uuid, + ( + SELECT + id + FROM + roles + WHERE + "name" = 'admin' + ), + '/useradmin' + ); + +INSERT INTO + role_permissions (created_by, updated_by, role_id, item) +VALUES + ( + user_uuid, + user_uuid, + ( + SELECT + id + FROM + roles + WHERE + "name" = 'admin' + ), + '/users' + ); + +INSERT INTO + role_permissions (created_by, updated_by, role_id, item) +VALUES + ( + user_uuid, + user_uuid, + ( + SELECT + id + FROM + roles + WHERE + "name" = 'calendar' + ), + '/calendar' + ); + +INSERT INTO + role_permissions (created_by, updated_by, role_id, item) +VALUES + ( + user_uuid, + user_uuid, + ( + SELECT + id + FROM + roles + WHERE + "name" = 'normal' + ), + '/wishlist' + ); + +INSERT INTO + role_permissions (created_by, updated_by, role_id, item) +VALUES + ( + user_uuid, + user_uuid, + ( + SELECT + id + FROM + roles + WHERE + "name" = 'normal' + ), + '/giftexchange' + ); + +end $$; \ No newline at end of file diff --git a/backend/src/calendar.rs b/backend/src/calendar.rs index 786b7ba..f7b3ed7 100644 --- a/backend/src/calendar.rs +++ b/backend/src/calendar.rs @@ -2,11 +2,11 @@ use askama::Template; use askama_axum::{IntoResponse, Response}; use axum::{ extract::{Path, State}, - response::{Html, Json, Redirect}, + response::{Html, Redirect}, Extension, }; use http::StatusCode; -use sqlx::SqlitePool; +use sqlx::PgPool; use crate::{ middlewares::is_authorized, @@ -47,7 +47,7 @@ struct CalendarTemplate { pub async fn calendar( Extension(user_data): Extension>, - State(db_pool): State, + State(db_pool): State, ) -> impl IntoResponse { // Is the user logged in? let logged_in = user_data.is_some(); @@ -89,7 +89,7 @@ pub async fn calendar( pub async fn get_events( Path(calendar): Path, - State(db_pool): State, + State(db_pool): State, Extension(user_data): Extension>, ) -> String { println!("Calendar: {}", calendar); @@ -101,8 +101,8 @@ pub async fn get_events( if logged_in { // Extract the user data. - let user = user_data.as_ref().unwrap().clone(); - let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default(); + let _user = user_data.as_ref().unwrap().clone(); + let _userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default(); if is_authorized("/calendar", user_data, db_pool.clone()).await { // Get requested calendar events from database diff --git a/backend/src/google_oauth.rs b/backend/src/google_oauth.rs index 3db24b2..1a18e3c 100644 --- a/backend/src/google_oauth.rs +++ b/backend/src/google_oauth.rs @@ -17,8 +17,7 @@ use oauth2::{ TokenResponse, TokenUrl, }; -use chrono::Utc; -use sqlx::SqlitePool; +use sqlx::PgPool; use std::collections::HashMap; use uuid::Uuid; @@ -61,7 +60,7 @@ fn get_client(hostname: String) -> Result { pub async fn login( Extension(user_data): Extension>, Query(mut params): Query>, - State(db_pool): State, + State(db_pool): State, Host(hostname): Host, ) -> Result { if user_data.is_some() { @@ -89,21 +88,22 @@ pub async fn login( .set_pkce_challenge(pkce_code_challenge) .url(); - sqlx::query( - "INSERT INTO oauth2_state_storage (csrf_state, pkce_code_verifier, return_url) VALUES (?, ?, ?);", + sqlx::query!( + "INSERT INTO oauth2_state_storage (csrf_state, pkce_code_verifier, return_url) VALUES ($1, $2, $3);",csrf_state.secret(), pkce_code_verifier.secret(), return_url ) - .bind(csrf_state.secret()) - .bind(pkce_code_verifier.secret()) - .bind(return_url) .execute(&db_pool) - .await?; + .await + .map_err(|e| { + eprintln!("Error inserting into oauth2_state_storage: {}", e); + AppError::new("Error inserting into oauth2_state_storage") + })?; Ok(Redirect::to(authorize_url.as_str())) } pub async fn google_auth_return( Query(mut params): Query>, - State(db_pool): State, + State(db_pool): State, cookie: Option>, Host(hostname): Host, ) -> Result { @@ -120,7 +120,7 @@ pub async fn google_auth_return( ); let query: (String, String) = sqlx::query_as( - r#"DELETE FROM oauth2_state_storage WHERE csrf_state = ? RETURNING pkce_code_verifier,return_url"#, + r#"DELETE FROM oauth2_state_storage WHERE csrf_state = $1 RETURNING pkce_code_verifier,return_url"#, ) .bind(state.secret()) .fetch_one(&db_pool) @@ -130,6 +130,7 @@ pub async fn google_auth_return( let _return_url = query.1; let pkce_code_verifier = PkceCodeVerifier::new(pkce_code_verifier); + // Exchange the code with a token. let client = get_client(hostname)?; let token_response = tokio::task::spawn_blocking(move || { @@ -185,21 +186,15 @@ pub async fn google_auth_return( // Check if user exists in database // If not, create a new user - let query: Result<(i64,), _> = sqlx::query_as(r#"SELECT id FROM users WHERE email=?"#) + let query: Result<(uuid::Uuid,), _> = sqlx::query_as(r#"SELECT id FROM users WHERE email=$1"#) .bind(email.as_str()) .fetch_one(&db_pool) .await; let user_id = if let Ok(query) = query { query.0 } else { - let now = Utc::now().timestamp(); - // Add user - let query: (i64,) = sqlx::query_as("INSERT INTO users (created_at, created_by, updated_at, updated_by, email, name, family_name, given_name) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id") - .bind(now) - .bind(0 as i64)// Created by system - .bind(now) - .bind(0 as i64) // Updated by system + let query: (uuid::Uuid,) = sqlx::query_as(r#"INSERT INTO users (created_by, updated_by, email, name, family_name, given_name) VALUES ((SELECT id FROM users WHERE "name" = 'admin'), (SELECT id FROM users WHERE "name" = 'admin'), $1, $2, $3, $4) RETURNING id"#) .bind(email.clone()) .bind(name.clone()) .bind(family_name.clone()) @@ -208,13 +203,8 @@ pub async fn google_auth_return( .await?; // Add public role - sqlx::query("INSERT INTO user_roles (created_at, created_by, updated_at, updated_by, user_id, role_id) VALUES (?, ?, ?, ?, ?, ?)") - .bind(now) - .bind(0 as i64)// Created by system - .bind(now) - .bind(0 as i64) // Updated by system + sqlx::query(r#"INSERT INTO user_roles (created_by, updated_by, user_id, role_id) VALUES ((SELECT id FROM users WHERE "name" = 'admin'), (SELECT id FROM users WHERE "name" = 'admin'), $1, (SELECT id FROM roles WHERE "name" = 'public'))"#) .bind(query.0) - .bind("1") .execute(&db_pool) .await?; @@ -242,18 +232,15 @@ pub async fn google_auth_return( session_token ) ).map_err(|_| AppError::new("Failed to create session token header"))?; - let now = Utc::now().timestamp(); sqlx::query( "INSERT INTO user_sessions (session_token_p1, session_token_p2, user_id, created_at, expires_at) - VALUES (?, ?, ?, ?, ?);", + VALUES ($1, $2, $3, now(), now() + interval '1 day');", ) .bind(session_token_p1) .bind(session_token_p2) .bind(user_id) // Set user to anonymous - .bind(now) - .bind(now + 60 * 60 * 24) .execute(&db_pool) .await?; } @@ -263,12 +250,12 @@ pub async fn google_auth_return( pub async fn logout( cookie: Option>, - State(db_pool): State, + State(db_pool): State, ) -> Result { if let Some(cookie) = cookie { if let Some(session_token) = cookie.get("session_token") { let session_token: Vec<&str> = session_token.split('_').collect(); - let _ = sqlx::query("DELETE FROM user_sessions WHERE session_token_1 = ?") + let _ = sqlx::query("DELETE FROM user_sessions WHERE session_token_1 = $1") .bind(session_token[0]) .execute(&db_pool) .await; diff --git a/backend/src/main.rs b/backend/src/main.rs index 1fe020e..41e59d9 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -3,9 +3,10 @@ use axum::{ routing::{get, get_service}, Extension, Router, }; +use dotenvy::var; use secret_gift_exchange::{giftexchange, giftexchange_save, giftexchanges}; -use sqlx::migrate::Migrator; -use sqlx::{sqlite::SqlitePoolOptions, SqlitePool}; +use sqlx::{migrate::Migrator, sqlite::SqlitePoolOptions, sqlite::SqliteRow, Row, SqlitePool}; +use sqlx::{postgres::PgPoolOptions, PgPool}; use std::net::SocketAddr; use tower_http::services::ServeDir; @@ -15,22 +16,26 @@ mod error_handling; mod google_oauth; mod middlewares; mod routes; +mod secret_gift_exchange; mod user; mod wishlist; -mod secret_gift_exchange; +use calendar::{calendar, get_events}; use error_handling::AppError; use google_oauth::{google_auth_return, login, logout}; use middlewares::inject_user_data; use routes::{about, contact, dashboard, index, profile, user_profile, useradmin}; use user::{add_user_role, delete_user_role, UserData}; -use calendar::{calendar, get_events}; -use wishlist::{user_wishlist, user_wishlist_add, user_wishlist_add_item, user_wishlist_bought_item, user_wishlist_delete_item, user_wishlist_edit_item, user_wishlist_received_item, user_wishlist_returned_item, user_wishlist_save_item, wishlists}; +use wishlist::{ + user_wishlist, user_wishlist_add, user_wishlist_add_item, user_wishlist_bought_item, + user_wishlist_delete_item, user_wishlist_edit_item, user_wishlist_received_item, + user_wishlist_returned_item, user_wishlist_save_item, wishlists, +}; //use email::send_emails; #[derive(Clone)] pub struct AppState { - pub db_pool: SqlitePool, + pub db_pool: PgPool, } #[tokio::main] @@ -38,14 +43,11 @@ async fn main() { // initialize tracing tracing_subscriber::fmt::init(); - let db_pool = SqlitePoolOptions::new() - .max_connections(5) - .connect("sqlite://db/db.sqlite3") - .await; + // Get the server settings from the env file + let database_url = var("DATABASE_URL").expect("DATABASE_URL not set"); + let db_pool = PgPoolOptions::new().connect(&database_url).await.unwrap(); - let app_state = AppState { - db_pool: db_pool.expect("Failed to get db_pool"), - }; + let app_state = AppState { db_pool: db_pool }; static MIGRATOR: Migrator = sqlx::migrate!(); @@ -54,39 +56,74 @@ async fn main() { .await .expect("Failed to run migrations"); + // Copy from old sqlite database if it exists + if let Ok(source_db_url) = var("SOURCE_DB_URL") { + let sdb_pool = SqlitePoolOptions::new() + .max_connections(5) + .connect(&source_db_url) + .await + .unwrap(); + + copy_database(&sdb_pool, &app_state.db_pool).await; + } else { + println!("SOURCE_DB_URL not set"); + } + let user_data: Option = None; // build our application with some routes let app = Router::new() .route("/dashboard", get(dashboard)) - // User .route("/profile", get(profile)) .route("/useradmin", get(useradmin)) .route("/users/:user_id", get(user_profile)) .route("/roles/:user_id/:role_id/add", get(add_user_role)) - .route("/roles/:user_id/:user_role_id/delete", get(delete_user_role)) - + .route( + "/roles/:user_id/:user_role_id/delete", + get(delete_user_role), + ) // Calendar .route("/calendar", get(calendar)) .route("/getevents/:calendar", get(get_events)) - // Wishlist .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)) - .route("/userwishlist/edit/:item_id", get(user_wishlist_edit_item).post(user_wishlist_save_item)) - .route("/userwishlist/bought/:user_id", get(user_wishlist_bought_item)) - .route("/userwishlist/received/:user_id", get(user_wishlist_received_item)) - .route("/userwishlist/delete/:item_id", get(user_wishlist_delete_item)) - .route("/userwishlist/returned/:item_id", get(user_wishlist_returned_item)) - + .route( + "/userwishlist/add/:user_id", + get(user_wishlist_add).post(user_wishlist_add_item), + ) + .route( + "/userwishlist/edit/:item_id", + get(user_wishlist_edit_item).post(user_wishlist_save_item), + ) + .route( + "/userwishlist/bought/:user_id", + get(user_wishlist_bought_item), + ) + .route( + "/userwishlist/received/:user_id", + get(user_wishlist_received_item), + ) + .route( + "/userwishlist/delete/:item_id", + get(user_wishlist_delete_item), + ) + .route( + "/userwishlist/returned/:item_id", + get(user_wishlist_returned_item), + ) // Secret Gift Exchange - Not ready for public use yet .route("/giftexchanges", get(giftexchanges)) - .route("/giftexchange/:giftexchange_id", get(giftexchange).post(giftexchange_save)) - - .nest_service("/assets", ServeDir::new("templates/assets") - .fallback(get_service(ServeDir::new("templates/assets")))) + .route( + "/giftexchange/:giftexchange_id", + get(giftexchange).post(giftexchange_save), + ) + .nest_service( + "/assets", + ServeDir::new("templates/assets") + .fallback(get_service(ServeDir::new("templates/assets"))), + ) .route("/", get(index)) .route("/about", get(about)) .route("/contactus", get(contact)) @@ -112,3 +149,169 @@ async fn main() { .await .unwrap(); } + +async fn copy_database(sdb_pool: &SqlitePool, db_pool: &PgPool) { + // Copy users + let users = sqlx::query( + r#"select + datetime(u.created_at, 'unixepoch'), + coalesce(cb.email, 'admin@jean-marie.ca') as created_by_email, + datetime(u.updated_at, 'unixepoch'), + coalesce(ub.email, 'admin@jean-marie.ca') as updated_by_email, + u.email, + u.name, + u.family_name, + u.given_name + from users u + left join users cb on cb.id = u.created_by + left join users ub on ub.id = u.updated_by;"#, + ) + .fetch_all(sdb_pool) + .await + .expect("Failed to copy users from SQLite to Postgres"); + + println!("\nCopying {} users", users.len()); + + for user in users { + if let ( + Ok(created_at), + Ok(created_by), + Ok(updated_at), + Ok(updated_by), + Ok(email), + Ok(name), + Ok(family_name), + Ok(given_name), + ) = ( + user.try_get::(0), + user.try_get::(1), + user.try_get::(2), + user.try_get::(3), + user.try_get::(4), + user.try_get::(5), + user.try_get::(6), + user.try_get::(7), + ) { + let result = sqlx::query( + r#"insert into users (created_at, created_by, updated_at, updated_by, email, name, family_name, given_name) + values ($1, (select id from users where email =$2), $3, (select id from users where email =$4), $5, $6, $7, $8)"# + ) + .bind(created_at) + .bind(created_by) + .bind(updated_at) + .bind(updated_by) + .bind(email) + .bind(name) + .bind(family_name) + .bind(given_name) + .execute(db_pool) + .await; + + if let Err(e) = result { + println!("Error: {}", e); + } + } + } + + // Copy user roles + let user_roles = sqlx::query( + r#"select + datetime(ur.created_at, 'unixepoch'), + coalesce(cb.email, 'admin@jean-marie.ca') as created_by_email, + datetime(ur.updated_at, 'unixepoch'), + coalesce(ub.email, 'admin@jean-marie.ca') as updated_by_email, + u.email as user_email, + r.name as role_name + from user_roles ur + left join users cb on cb.id = ur.created_by + left join users ub on ub.id = ur.updated_by + join users u on u.id = ur.user_id + join roles r on r.id = ur.role_id;"#, + ) + .fetch_all(sdb_pool) + .await + .expect("Failed to copy user roles from SQLite to Postgres"); + + println!("\nCopying {} user roles", user_roles.len()); + + for user_role in user_roles { + if let ( + Ok(created_at), + Ok(created_by), + Ok(updated_at), + Ok(updated_by), + Ok(user_email), + Ok(role_name), + ) = ( + user_role.try_get::(0), + user_role.try_get::(1), + user_role.try_get::(2), + user_role.try_get::(3), + user_role.try_get::(4), + user_role.try_get::(5), + ) { + let result = sqlx::query( + r#"insert into user_roles (created_at, created_by, updated_at, updated_by, user_id, role_id) + values ($1, (select id from users where email=$2), $3, (select id from users where email=$4), (select id from users where email=$5), (select id from roles where name=$6))"# + ) + .bind(created_at) + .bind(created_by) + .bind(updated_at) + .bind(updated_by) + .bind(user_email) + .bind(role_name) + .execute(db_pool) + .await; + + if let Err(e) = result { + println!("Error: {}", e); + } + } + } + + // Copy wishlistitems + let wishlistitems = sqlx::query( + r#"select + datetime(wi.created_at, 'unixepoch'), + coalesce(cb.email, 'admin@jean-marie.ca') as created_by_email, + datetime(wi.updated_at, 'unixepoch'), + coalesce(ub.email, 'admin@jean-marie.ca') as updated_by_email, + u.email as user_email, + wi.item, + wi.item_url, + pb.email, + datetime(wi.received_at, 'unixepoch') + from wishlist_items wi + left join users cb on cb.id = wi.created_by + left join users ub on ub.id = wi.updated_by + left join users pb on pb.id = wi.purchased_by + join users u on u.id = wi.user_id;"#, + ) + .fetch_all(sdb_pool) + .await + .expect("Failed to copy wishlistitems from SQLite to Postgres"); + + println!("\nCopying {} wishlistitems", wishlistitems.len()); + + for wishlistitem in wishlistitems { + let result = sqlx::query( + r#"insert into wishlist_items (created_at, created_by, updated_at, updated_by, user_id, item, item_url, purchased_by, received_at) + values ($1, (select id from users where email=$2), $3, (select id from users where email=$4), (select id from users where email=$5), $6, $7, (select id from users where email=$8), $9)"# + ) + .bind(wishlistitem.try_get::(0).unwrap()) + .bind(wishlistitem.try_get::(1).unwrap()) + .bind(wishlistitem.try_get::(2).unwrap()) + .bind(wishlistitem.try_get::(3).unwrap()) + .bind(wishlistitem.try_get::(4).unwrap()) + .bind(wishlistitem.try_get::(5).unwrap()) + .bind(wishlistitem.try_get::(6).unwrap()) + .bind(wishlistitem.try_get::(7).unwrap()) + .bind(wishlistitem.try_get::(8).unwrap_or_default()) + .execute(db_pool) + .await; + + if let Err(e) = result { + println!("Error: {}", e); + } + } +} diff --git a/backend/src/middlewares.rs b/backend/src/middlewares.rs index 8d81472..c60f2ba 100644 --- a/backend/src/middlewares.rs +++ b/backend/src/middlewares.rs @@ -1,20 +1,14 @@ use std::path::Path; use super::{AppError, UserData}; -use axum::{ - body::Body, - extract::State, - http::Request, - middleware::Next, - response::IntoResponse, -}; +use axum::{body::Body, extract::State, http::Request, middleware::Next, response::IntoResponse}; use axum_extra::TypedHeader; use chrono::Utc; use headers::Cookie; -use sqlx::SqlitePool; +use sqlx::PgPool; pub async fn inject_user_data( - State(db_pool): State, + State(db_pool): State, cookie: Option>, mut request: Request, next: Next, @@ -22,8 +16,8 @@ pub async fn inject_user_data( if let Some(cookie) = cookie { if let Some(session_token) = cookie.get("session_token") { let session_token: Vec<&str> = session_token.split('_').collect(); - let query: Result<(i64, i64, String), _> = sqlx::query_as( - r#"SELECT user_id,expires_at,session_token_p2 FROM user_sessions WHERE session_token_p1=?"#, + let query: Result<(uuid::Uuid, chrono::NaiveDateTime, String), _> = sqlx::query_as( + r#"SELECT user_id,expires_at,session_token_p2 FROM user_sessions WHERE session_token_p1=$1"#, ) .bind(session_token[0]) .fetch_one(&db_pool) @@ -44,14 +38,14 @@ pub async fn inject_user_data( ) { let id = query.0; let expires_at = query.1; - if expires_at > Utc::now().timestamp() { - let row = sqlx::query_as!( - UserData, - "SELECT * FROM users WHERE id = ?", - id - ) + if expires_at > Utc::now().naive_local() { + let row: UserData = sqlx::query_as( + r#"SELECT id, created_at, created_by, updated_at, updated_by, email, name, family_name, given_name FROM users WHERE id = $1"#, + ) + .bind(id) .fetch_one(&db_pool) - .await?; + .await + .unwrap(); request.extensions_mut().insert(Some(UserData { id: row.id, @@ -62,7 +56,7 @@ pub async fn inject_user_data( email: row.email, name: row.name, family_name: row.family_name, - given_name: row.given_name + given_name: row.given_name, })); } } @@ -75,9 +69,9 @@ pub async fn inject_user_data( Ok(next.run(request).await) } -pub async fn is_authorized(path: &str, user_data: Option, db_pool: SqlitePool) -> bool { +pub async fn is_authorized(path: &str, user_data: Option, db_pool: PgPool) -> bool { if let Some(user_data) = user_data { - let query: Result<(i64,), _> = match path { + let query: Result<(uuid::Uuid,), _> = match path { "/profile" => { return true; } @@ -85,10 +79,11 @@ pub async fn is_authorized(path: &str, user_data: Option, db_pool: Sql // loop through path to find a permission let mut remaining_path = Path::new(path); loop { - let query: Result<(String,), _> = sqlx::query_as(r#"select r.item from role_permissions r where item = ?"#) - .bind(remaining_path.to_str().unwrap()) - .fetch_one(&db_pool) - .await; + let query: Result<(String,), _> = + sqlx::query_as(r#"select r.item from role_permissions r where item = $1"#) + .bind(remaining_path.to_str().unwrap()) + .fetch_one(&db_pool) + .await; if let Ok(query) = query { if query.0 != "" { break; @@ -99,7 +94,7 @@ pub async fn is_authorized(path: &str, user_data: Option, db_pool: Sql } remaining_path = remaining_path.parent().unwrap(); } - sqlx::query_as(r#"select u.id from role_permissions r join user_roles ur on ur.role_id = r.role_id join users u on u.id = ur.user_id where item = ? and email = ?"#) + sqlx::query_as(r#"select u.id from role_permissions r join user_roles ur on ur.role_id = r.role_id join users u on u.id = ur.user_id where item = $1 and email = $2"#) .bind(remaining_path.to_str().unwrap()) .bind(user_data.email.as_str()) .fetch_one(&db_pool) @@ -114,4 +109,4 @@ pub async fn is_authorized(path: &str, user_data: Option, db_pool: Sql } } return false; -} \ No newline at end of file +} diff --git a/backend/src/routes.rs b/backend/src/routes.rs index bdba892..61455d4 100644 --- a/backend/src/routes.rs +++ b/backend/src/routes.rs @@ -5,7 +5,7 @@ use axum::{ Extension, }; use http::StatusCode; -use sqlx::SqlitePool; +use sqlx::PgPool; use crate::{ middlewares::is_authorized, @@ -67,7 +67,7 @@ struct DashboardTemplate { } pub async fn index( - State(db_pool): State, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { // Is the user logged in? @@ -93,7 +93,7 @@ pub async fn index( } pub async fn dashboard( - State(db_pool): State, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { // Is the user logged in? @@ -127,7 +127,7 @@ pub async fn dashboard( /// Handles the profile page. pub async fn profile( - State(db_pool): State, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { // Is the user logged in? @@ -158,8 +158,8 @@ pub async fn profile( } pub async fn user_profile( - Path(user_id): Path, - State(db_pool): State, + Path(user_id): Path, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { // Is the user logged in? @@ -171,7 +171,8 @@ pub async fn user_profile( let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default(); // Extract the user data. - let profile = sqlx::query_as!(UserData, "SELECT * FROM users WHERE id = ?", user_id) + let profile = sqlx::query_as( "SELECT * FROM users WHERE id = $1") + .bind(user_id) .fetch_one(&db_pool) .await .unwrap(); @@ -215,7 +216,7 @@ struct UserAdminTemplate { pub async fn useradmin( Extension(user_data): Extension>, - State(db_pool): State, + State(db_pool): State, ) -> impl IntoResponse { // Is the user logged in? let logged_in = user_data.is_some(); @@ -261,17 +262,7 @@ pub async fn about(Extension(user_data): Extension>) -> impl In let logged_in = user_data.is_some(); // Set empty user - let mut user = UserData { - id: 0, - email: "".to_string(), - created_at: 0, - created_by: 0, - updated_at: 0, - updated_by: 0, - name: "".to_string(), - family_name: "".to_string(), - given_name: "".to_string(), - }; + let mut user = UserData::default(); if logged_in { // Extract the user data. @@ -294,17 +285,7 @@ pub async fn contact(Extension(user_data): Extension>) -> impl let logged_in = user_data.is_some(); // Set empty user - let mut user = UserData { - id: 0, - email: "".to_string(), - created_at: 0, - created_by: 0, - updated_at: 0, - updated_by: 0, - name: "".to_string(), - family_name: "".to_string(), - given_name: "".to_string(), - }; + let mut user = UserData::default(); if logged_in { // Extract the user data. diff --git a/backend/src/secret_gift_exchange.rs b/backend/src/secret_gift_exchange.rs index 8083687..8dab35d 100644 --- a/backend/src/secret_gift_exchange.rs +++ b/backend/src/secret_gift_exchange.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use askama::Template; use askama_axum::{IntoResponse, Response}; use axum::{ @@ -9,10 +7,9 @@ use axum::{ Extension, Form, Json, RequestExt, }; use axum_extra::response::Html; -use chrono::Utc; use http::{header::CONTENT_TYPE, StatusCode}; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, SqlitePool}; +use sqlx::{FromRow, PgPool}; use crate::{ middlewares::is_authorized, @@ -99,7 +96,7 @@ struct GiftExchangesTemplate { pub async fn giftexchanges( Extension(user_data): Extension>, - State(db_pool): State, + State(db_pool): State, ) -> impl IntoResponse { // Is the user logged in? let logged_in = user_data.is_some(); @@ -159,7 +156,7 @@ struct GiftExchangeTemplate { pub async fn giftexchange( Path(exchange_id): Path, Extension(user_data): Extension>, - State(db_pool): State, + State(db_pool): State, ) -> impl IntoResponse { // Is the user logged in? let logged_in = user_data.is_some(); @@ -174,11 +171,9 @@ pub async fn giftexchange( let user_roles = get_user_roles_display(userid, &db_pool.clone()).await; // Get gift exchange - let giftexchange = match sqlx::query_as!( - GiftExchange, - "SELECT * FROM gift_exchange WHERE id = ?", - exchange_id - ) + let giftexchange = match sqlx::query_as( + "SELECT * FROM gift_exchange WHERE id = ?") + .bind(exchange_id) .fetch_one(&db_pool) .await { @@ -188,7 +183,7 @@ pub async fn giftexchange( // Get participants let participants = sqlx::query_as::<_, UserData>( - "select * from users where users.id in (select participant_id from gift_exchange_participants where exchange_id = ?)", + "select * from users where users.id in (select participant_id from gift_exchange_participants where exchange_id = $1)", ) .bind(exchange_id) .fetch_all(&db_pool) @@ -197,7 +192,7 @@ pub async fn giftexchange( // Get non participants let non_participants = sqlx::query_as::<_, UserData>( - "select * from users where users.id not in (select participant_id from gift_exchange_participants where exchange_id = ?)", + "select * from users where users.id not in (select participant_id from gift_exchange_participants where exchange_id = $1)", ) .bind(exchange_id) .fetch_all(&db_pool) @@ -225,15 +220,15 @@ pub async fn giftexchange( pub struct ExchangeForm { name: String, exchange_date: String, - non_participants: Vec, + non_participants: Vec, } pub async fn giftexchange_save( - State(db_pool): State, + State(_db_pool): State, request: Request, ) -> impl IntoResponse { let content_type_header = request.headers().get(CONTENT_TYPE); - let content_type = content_type_header.and_then(|value| value.to_str().ok()); + let _content_type = content_type_header.and_then(|value| value.to_str().ok()); /* if let Some(content_type) = content_type { if content_type.starts_with("application/json") { diff --git a/backend/src/user.rs b/backend/src/user.rs index 73c68f5..347dc9a 100644 --- a/backend/src/user.rs +++ b/backend/src/user.rs @@ -4,21 +4,20 @@ use axum::{ response::Redirect, Extension, }; -use chrono::Utc; ///User related structs and functions use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, SqlitePool}; +use sqlx::{FromRow, PgPool}; use crate::middlewares::is_authorized; #[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)] pub struct UserData { - pub id: i64, - pub created_at: i64, - pub created_by: i64, - pub updated_at: i64, - pub updated_by: i64, + pub id: uuid::Uuid, + pub created_at: chrono::NaiveDateTime, + pub created_by: uuid::Uuid, + pub updated_at: chrono::NaiveDateTime, + pub updated_by: uuid::Uuid, pub email: String, pub name: String, pub family_name: String, @@ -27,58 +26,58 @@ pub struct UserData { #[derive(Serialize, Deserialize)] pub struct RoleData { - pub id: i64, - pub created_at: i64, - pub created_by: i64, - pub updated_at: i64, - pub updated_by: i64, + pub id: uuid::Uuid, + pub created_at: chrono::NaiveDateTime, + pub created_by: uuid::Uuid, + pub updated_at: chrono::NaiveDateTime, + pub updated_by: uuid::Uuid, pub name: String, pub description: String, } #[derive(Default, Clone, Debug, Serialize, Deserialize)] pub struct UserRoles { - pub id: i64, - pub created_at: i64, - pub created_by: i64, - pub updated_at: i64, - pub updated_by: i64, - pub user_id: i64, - pub role_id: i64, + pub id: uuid::Uuid, + pub created_at: chrono::NaiveDateTime, + pub created_by: uuid::Uuid, + pub updated_at: chrono::NaiveDateTime, + pub updated_by: uuid::Uuid, + pub user_id: uuid::Uuid, + pub role_id: uuid::Uuid, } #[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)] pub struct UserRolesDisplay { - pub id: i64, - pub created_at: i64, - pub created_by: i64, - pub updated_at: i64, - pub updated_by: i64, - pub user_id: i64, + pub id: uuid::Uuid, + pub created_at: chrono::NaiveDateTime, + pub created_by: uuid::Uuid, + pub updated_at: chrono::NaiveDateTime, + pub updated_by: uuid::Uuid, + pub user_id: uuid::Uuid, pub user_name: String, - pub role_id: i64, + pub role_id: uuid::Uuid, pub role_name: String, } #[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)] 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 id: uuid::Uuid, + pub created_at: chrono::NaiveDateTime, + pub created_by: uuid::Uuid, + pub updated_at: chrono::NaiveDateTime, + pub updated_by: uuid::Uuid, + pub user_id: uuid::Uuid, pub item: String, pub item_url: String, - pub purchased_by: i64, - pub received_at: i64, + pub purchased_by: Option, + pub received_at: Option, } /* -pub async fn get_user_roles(user_id: i64, db_pool: &SqlitePool) -> Vec { +pub async fn get_user_roles(user_id: i64, db_pool: &PgPool) -> Vec { // Get user roles let user_roles = sqlx::query_as( - r#"SELECT id, created_at, created_by, updated_at, updated_by, user_id, role_id FROM user_roles WHERE user_id = ?"# + r#"SELECT id, created_at, created_by, updated_at, updated_by, user_id, role_id FROM user_roles WHERE user_id = $1"# ) .bind(user_id) .fetch_all(db_pool) @@ -88,10 +87,10 @@ pub async fn get_user_roles(user_id: i64, db_pool: &SqlitePool) -> Vec Vec { +pub async fn get_user_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> Vec { // Get user roles let user_roles = sqlx::query_as( - r#"select ur.id, u.id as user_id, u.name as user_name, r.id as role_id, r.name as role_name, r.created_at, r.created_by, r.updated_at, r.updated_by from roles r join user_roles ur on ur.role_id = r.id join users u on u.id = ur.user_id WHERE ur.user_id = ?"# + r#"select ur.id, u.id as user_id, u.name as user_name, r.id as role_id, r.name as role_name, r.created_at, r.created_by, r.updated_at, r.updated_by from roles r join user_roles ur on ur.role_id = r.id join users u on u.id = ur.user_id WHERE ur.user_id = $1"# ) .bind(user_id) .fetch_all(db_pool) @@ -101,10 +100,10 @@ pub async fn get_user_roles_display(user_id: i64, db_pool: &SqlitePool) -> Vec Vec { +pub async fn get_other_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> Vec { // Get roles user does not have let user_roles = sqlx::query_as( - r#"select 0 as id, r.created_at, r.created_by, r.updated_at, r.updated_by, ? as user_id, '' as user_name, r.id as role_id, r.name as role_name from roles r where r.id not in (select ur.role_id from user_roles ur where ur.user_id = ?)"# + r#"select r.id as id, r.created_at, r.created_by, r.updated_at, r.updated_by, $1 as user_id, '' as user_name, r.id as role_id, r.name as role_name from roles r where r.id not in (select ur.role_id from user_roles ur where ur.user_id = $2)"# ) .bind(user_id.clone()) .bind(user_id) @@ -116,17 +115,13 @@ pub async fn get_other_roles_display(user_id: i64, db_pool: &SqlitePool) -> Vec< } pub async fn add_user_role( - Path((user_id, role_id)): Path<(i64, i64)>, - State(db_pool): State, + Path((user_id, role_id)): Path<(uuid::Uuid, uuid::Uuid)>, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { if is_authorized("/roles", user_data.clone(), db_pool.clone()).await { - let now = Utc::now().timestamp(); - - sqlx::query("INSERT INTO user_roles (created_at, created_by, updated_at, updated_by, user_id, role_id) VALUES (?, ?, ?, ?, ?, ?)") - .bind(now)// Created now + sqlx::query("INSERT INTO user_roles (created_by, updated_by, user_id, role_id) VALUES ($1, $2, $3, $4)") .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(role_id) @@ -144,12 +139,12 @@ pub async fn add_user_role( } pub async fn delete_user_role( - Path((user_id, user_role_id)): Path<(i64, i64)>, - State(db_pool): State, + Path((user_id, user_role_id)): Path<(uuid::Uuid, uuid::Uuid)>, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { if is_authorized("/roles", user_data, db_pool.clone()).await { - sqlx::query("DELETE FROM user_roles WHERE id = ?") + sqlx::query("DELETE FROM user_roles WHERE id = $1") .bind(user_role_id) .execute(&db_pool) .await @@ -162,11 +157,11 @@ pub async fn delete_user_role( } } -pub async fn get_user_wishlist_item_by_id(item_id: i64, db_pool: &SqlitePool) -> UserWishlistItem { +pub async fn get_user_wishlist_item_by_id(item_id: uuid::Uuid, db_pool: &PgPool) -> UserWishlistItem { // Get wish list items for the user let user_wishlist_item = sqlx::query_as( r#"select id, created_at, created_by, updated_at, updated_by, user_id, item, item_url, purchased_by, received_at - from wishlist_items where id = ?"# + from wishlist_items where id = $1"# ) .bind(item_id) .fetch_one(db_pool) @@ -176,11 +171,11 @@ pub async fn get_user_wishlist_item_by_id(item_id: i64, db_pool: &SqlitePool) -> user_wishlist_item } -pub async fn get_user_wishlist_items(user_id: i64, db_pool: &SqlitePool) -> Vec { +pub async fn get_user_wishlist_items(user_id: uuid::Uuid, db_pool: &PgPool) -> 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, received_at - from wishlist_items where user_id = ?"# + from wishlist_items where user_id = $1"# ) .bind(user_id) .fetch_all(db_pool) @@ -190,8 +185,8 @@ pub async fn get_user_wishlist_items(user_id: i64, db_pool: &SqlitePool) -> Vec< user_wishlist_items } -pub async fn get_useremails_by_role(role_name: String, db_pool: &SqlitePool) -> String { - let useremails: String = sqlx::query_scalar(r#"select group_concat(u.email) as email from user_roles ur, roles r, users u where u.id = ur.user_id and r.id = ur.role_id and r.name = ?"#) +pub async fn get_useremails_by_role(role_name: String, db_pool: &PgPool) -> String { + let useremails: String = sqlx::query_scalar(r#"select string_agg(u.email, ',') as email from user_roles ur, roles r, users u where u.id = ur.user_id and r.id = ur.role_id and r.name = $1"#) .bind(role_name) .fetch_one(db_pool) .await diff --git a/backend/src/wishlist.rs b/backend/src/wishlist.rs index 3572e4c..98b0915 100644 --- a/backend/src/wishlist.rs +++ b/backend/src/wishlist.rs @@ -8,7 +8,8 @@ use axum_extra::response::Html; use chrono::Utc; use http::StatusCode; use serde::Deserialize; -use sqlx::{Row, SqlitePool}; +use sqlx::{Row, PgPool}; +use uuid::Uuid; use crate::{ middlewares::is_authorized, @@ -47,7 +48,7 @@ struct WishListsTemplate { pub async fn wishlists( Extension(user_data): Extension>, - State(db_pool): State, + State(db_pool): State, ) -> impl IntoResponse { // Is the user logged in? let logged_in = user_data.is_some(); @@ -93,8 +94,8 @@ struct UserWishListTemplate { } pub async fn user_wishlist( - Path(user_id): Path, - State(db_pool): State, + Path(user_id): Path, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { // Is the user logged in? @@ -106,14 +107,15 @@ pub async fn user_wishlist( let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default(); // Extract the user data. - let person = sqlx::query_as!(UserData, "SELECT * FROM users WHERE id = ?", user_id) + let person = sqlx::query_as("SELECT * FROM users WHERE id = $1") + .bind(user_id) .fetch_one(&db_pool) .await .unwrap(); - if is_authorized("/wishlist", user_data, db_pool.clone()).await { - // Get user roles - let user_roles = get_user_roles_display(userid, &db_pool.clone()).await; + if is_authorized("/wishlist", user_data, db_pool.clone()).await { + // Get user roles + let user_roles = get_user_roles_display(userid, &db_pool.clone()).await; // Get user wishlist let person_wishlist_items = get_user_wishlist_items(user_id, &db_pool.clone()).await; @@ -150,8 +152,8 @@ struct UserWishListAddTemplate { } pub async fn user_wishlist_add( - Path(user_id): Path, - State(db_pool): State, + Path(user_id): Path, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { // Is the user logged in? @@ -163,7 +165,8 @@ pub async fn user_wishlist_add( let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default(); // Extract the user data. - let person = sqlx::query_as!(UserData, "SELECT * FROM users WHERE id = ?", user_id) + let person = sqlx::query_as("SELECT * FROM users WHERE id = $1") + .bind(user_id) .fetch_one(&db_pool) .await .unwrap(); @@ -199,19 +202,15 @@ pub struct ItemForm { } pub async fn user_wishlist_add_item( - Path(user_id): Path, - State(db_pool): State, + 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 + sqlx::query("insert into wishlist_items (created_by, updated_by, user_id, item, item_url) values ($1, $2, $3, $4, $5)") .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) @@ -237,8 +236,8 @@ struct UserWishListEditTemplate { } pub async fn user_wishlist_edit_item( - Path(item_id): Path, - State(db_pool): State, + Path(item_id): Path, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { // Is the user logged in? @@ -274,16 +273,16 @@ pub async fn user_wishlist_edit_item( } pub async fn user_wishlist_save_item( - Path(item_id): Path, - State(db_pool): State, + Path(item_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(); + let now = Utc::now().naive_local(); - sqlx::query("update wishlist_items set updated_at = ?, updated_by = ?, item = ?, item_url = ? where id = ?") + sqlx::query("update wishlist_items set updated_at = $1, updated_by = $2, item = $3, item_url = $4 where id = $5") .bind(now) // Updated now .bind(user_data.as_ref().unwrap().id) // Updated by current user .bind(item_form.item) @@ -302,28 +301,28 @@ pub async fn user_wishlist_save_item( } pub async fn user_wishlist_bought_item( - Path(user_id): Path, - State(db_pool): State, + Path(item_id): Path, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await { // Update item to purchased - sqlx::query("update wishlist_items set purchased_by = ? where id = ?") + sqlx::query("update wishlist_items set purchased_by = $1 where id = $2") .bind(user_data.as_ref().unwrap().id) // Created by current user - .bind(user_id) + .bind(item_id) .execute(&db_pool) .await .unwrap(); // Redirect to user wishlist // Extract the user data. - let row = sqlx::query("SELECT user_id FROM wishlist_items WHERE id = ?") - .bind(user_id) + let row = sqlx::query("SELECT user_id FROM wishlist_items WHERE id = $1") + .bind(item_id) .fetch_one(&db_pool) .await .unwrap(); - let userid = row.get::("user_id"); + let userid = row.get::("user_id"); let redirect_string = format!("/userwishlist/{userid}"); Redirect::to(&redirect_string).into_response() } else { @@ -332,15 +331,15 @@ pub async fn user_wishlist_bought_item( } pub async fn user_wishlist_received_item( - Path(user_id): Path, - State(db_pool): State, + Path(user_id): Path, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await { // Update item received time - let now = Utc::now().timestamp(); + let now = Utc::now().naive_local(); - sqlx::query("update wishlist_items set received_at = ? where id = ?") + sqlx::query("update wishlist_items set received_at = $1 where id = $2") .bind(now) // Received now .bind(user_id) .execute(&db_pool) @@ -357,12 +356,12 @@ pub async fn user_wishlist_received_item( } pub async fn user_wishlist_delete_item( - Path(item_id): Path, - State(db_pool): State, + Path(item_id): Path, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await { - sqlx::query("delete from wishlist_items where id = ?") + sqlx::query("delete from wishlist_items where id = $1") .bind(item_id) .execute(&db_pool) .await @@ -378,12 +377,12 @@ pub async fn user_wishlist_delete_item( } pub async fn user_wishlist_returned_item( - Path(item_id): Path, - State(db_pool): State, + Path(item_id): Path, + State(db_pool): State, Extension(user_data): Extension>, ) -> impl IntoResponse { if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await { - sqlx::query("update wishlist_items set purchased_by = 0 where id = ?") + sqlx::query("update wishlist_items set purchased_by = null where id = $1") .bind(item_id) .execute(&db_pool) .await @@ -391,13 +390,13 @@ pub async fn user_wishlist_returned_item( // Redirect to user wishlist // Extract the user data. - let row = sqlx::query("SELECT user_id FROM wishlist_items WHERE id = ?") + let row = sqlx::query("SELECT user_id FROM wishlist_items WHERE id = $1") .bind(item_id) .fetch_one(&db_pool) .await .unwrap(); - let profileid = row.get::("user_id"); + let profileid = row.get::("user_id"); let redirect_string = format!("/userwishlist/{profileid}"); Redirect::to(&redirect_string).into_response() } else { diff --git a/backend/templates/userwishlist.html b/backend/templates/userwishlist.html index 0fb8b3d..66a4bc0 100644 --- a/backend/templates/userwishlist.html +++ b/backend/templates/userwishlist.html @@ -25,38 +25,44 @@ {% for person_wishlist_item in person_wishlist_items %} {% if my_wishlist %} - {{ person_wishlist_item.item }} + {{ person_wishlist_item.item }} {% else %} - {{ person_wishlist_item.item }} + {{ person_wishlist_item.item }} {% endif %} {% if person_wishlist_item.item_url.len() > 0 %} - URL + URL {% else %} - + {% endif %} - {% if person_wishlist_item.received_at > 0 %} - Got it! - {% else %} - Not yet! - {% endif %} - + {% match person_wishlist_item.received_at %} + {% when None %} + Not yet! + {% when Some with (received_at) %} + Got it! + {% endmatch %} + {% if my_wishlist %} - {% if person_wishlist_item.received_at > 0 %} - Delete - {% else %} - Received - {% endif %} + {% match person_wishlist_item.received_at %} + {% when None %} + Received + {% when Some with (received_at) %} + Delete + {% endmatch %} {% else %} - {% if person_wishlist_item.purchased_by == user.id %} - Return - {% else if person_wishlist_item.purchased_by > 0 %} - Purchased - {% else %} - Bought - {% endif %} + {% match person_wishlist_item.purchased_by %} + {% when Some with (purchased_by) %} + {% if purchased_by.clone() == user.id %} + Return + {% else %} + Purchased + {% endif %} + {% when None %} + Bought + {% endmatch %} {% endif %} + {% endfor %}