Compare commits

..

No commits in common. "4ff14e6fa108d5eb8c56cc750b5011067284366e" and "e3e7e4442b50aba5b1d0b844ae6fcf4e6d037ac3" have entirely different histories.

31 changed files with 448 additions and 606 deletions

2
.gitignore vendored
View File

@ -2,5 +2,3 @@ backend/target
backend/db
backend/id_rsa
backend/id_rsa.pub
backend/sqlite3.env
backend/.env

7
backend/.env Normal file
View File

@ -0,0 +1,7 @@
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

8
backend/Cargo.lock generated
View File

@ -2603,7 +2603,6 @@ dependencies = [
"atoi",
"byteorder",
"bytes",
"chrono",
"crc",
"crossbeam-queue",
"either",
@ -2632,7 +2631,6 @@ dependencies = [
"tokio-stream",
"tracing",
"url",
"uuid",
]
[[package]]
@ -2685,7 +2683,6 @@ dependencies = [
"bitflags 2.6.0",
"byteorder",
"bytes",
"chrono",
"crc",
"digest",
"dotenvy",
@ -2714,7 +2711,6 @@ dependencies = [
"stringprep",
"thiserror",
"tracing",
"uuid",
"whoami",
]
@ -2728,7 +2724,6 @@ dependencies = [
"base64 0.22.1",
"bitflags 2.6.0",
"byteorder",
"chrono",
"crc",
"dotenvy",
"etcetera",
@ -2754,7 +2749,6 @@ dependencies = [
"stringprep",
"thiserror",
"tracing",
"uuid",
"whoami",
]
@ -2765,7 +2759,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680"
dependencies = [
"atoi",
"chrono",
"flume",
"futures-channel",
"futures-core",
@ -2780,7 +2773,6 @@ dependencies = [
"sqlx-core",
"tracing",
"url",
"uuid",
]
[[package]]

View File

@ -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 = ["postgres", "runtime-tokio", "macros", "chrono", "uuid"] }
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "macros"] }
uuid = { version = "1.10", features = ["v4"] }
dotenvy = "0.15"
constant_time_eq = "0.3"

View File

@ -0,0 +1 @@
-- Add down migration script here

View File

@ -0,0 +1,58 @@
-- 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
);

View File

@ -0,0 +1 @@
-- Add down migration script here

View File

@ -0,0 +1,6 @@
-- 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');

View File

@ -0,0 +1 @@
-- Add down migration script here

View File

@ -0,0 +1,12 @@
-- 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');

View File

@ -0,0 +1,9 @@
-- 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';

View File

@ -0,0 +1,9 @@
-- 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');

View File

@ -0,0 +1,4 @@
-- Add down migration script here
drop table if exists `wishlist_items`;
delete from `role_permissions` where id = 9;

View File

@ -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')

View File

@ -0,0 +1,2 @@
-- Add down migration script here
alter table wishlist_items drop column received_at;

View File

@ -0,0 +1,2 @@
-- Add up migration script here
alter table wishlist_items add column received_at integer null;

View File

@ -0,0 +1,5 @@
-- Add down migration script here
drop table gift_exchange;
drop table gift_exchange_participants;
delete from role_permissions where item = '/giftexchange';

View File

@ -0,0 +1,28 @@
-- 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')

View File

@ -0,0 +1,5 @@
-- Add down migration script here
drop table if exists `calendar_events`;
drop table if exists `calendar_event_types`;
drop table if exists `calendar`;

View File

@ -0,0 +1,42 @@
-- 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
);

View File

@ -1,13 +0,0 @@
-- Drop Postgres tables
drop table oauth2_state_storage;
drop table user_sessions;
drop table users;
drop table roles;
drop table user_roles;
drop table role_permissions;
drop table wishlist_items;
drop table gift_exchange;
drop table gift_exchange_participants;
drop table calendar;
drop table calendar_event_types;
drop table calendar_events;

View File

@ -1,384 +0,0 @@
-- 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 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(255) null,
item_url varchar(255) 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 $$;

View File

@ -2,11 +2,11 @@ use askama::Template;
use askama_axum::{IntoResponse, Response};
use axum::{
extract::{Path, State},
response::{Html, Redirect},
response::{Html, Json, Redirect},
Extension,
};
use http::StatusCode;
use sqlx::PgPool;
use sqlx::SqlitePool;
use crate::{
middlewares::is_authorized,
@ -47,7 +47,7 @@ struct CalendarTemplate {
pub async fn calendar(
Extension(user_data): Extension<Option<UserData>>,
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
) -> 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<String>,
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> 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

View File

@ -17,7 +17,8 @@ use oauth2::{
TokenResponse, TokenUrl,
};
use sqlx::PgPool;
use chrono::Utc;
use sqlx::SqlitePool;
use std::collections::HashMap;
use uuid::Uuid;
@ -60,7 +61,7 @@ fn get_client(hostname: String) -> Result<BasicClient, AppError> {
pub async fn login(
Extension(user_data): Extension<Option<UserData>>,
Query(mut params): Query<HashMap<String, String>>,
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
Host(hostname): Host,
) -> Result<Redirect, AppError> {
if user_data.is_some() {
@ -88,22 +89,21 @@ 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 ($1, $2, $3);",csrf_state.secret(), pkce_code_verifier.secret(), return_url
sqlx::query(
"INSERT INTO oauth2_state_storage (csrf_state, pkce_code_verifier, return_url) VALUES (?, ?, ?);",
)
.bind(csrf_state.secret())
.bind(pkce_code_verifier.secret())
.bind(return_url)
.execute(&db_pool)
.await
.map_err(|e| {
eprintln!("Error inserting into oauth2_state_storage: {}", e);
AppError::new("Error inserting into oauth2_state_storage")
})?;
.await?;
Ok(Redirect::to(authorize_url.as_str()))
}
pub async fn google_auth_return(
Query(mut params): Query<HashMap<String, String>>,
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
cookie: Option<TypedHeader<Cookie>>,
Host(hostname): Host,
) -> Result<impl IntoResponse, AppError> {
@ -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 = $1 RETURNING pkce_code_verifier,return_url"#,
r#"DELETE FROM oauth2_state_storage WHERE csrf_state = ? RETURNING pkce_code_verifier,return_url"#,
)
.bind(state.secret())
.fetch_one(&db_pool)
@ -130,7 +130,6 @@ 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 || {
@ -186,15 +185,21 @@ pub async fn google_auth_return(
// Check if user exists in database
// If not, create a new user
let query: Result<(uuid::Uuid,), _> = sqlx::query_as(r#"SELECT id FROM users WHERE email=$1"#)
let query: Result<(i64,), _> = sqlx::query_as(r#"SELECT id FROM users WHERE email=?"#)
.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: (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"#)
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
.bind(email.clone())
.bind(name.clone())
.bind(family_name.clone())
@ -203,8 +208,13 @@ pub async fn google_auth_return(
.await?;
// Add public role
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'))"#)
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
.bind(query.0)
.bind("1")
.execute(&db_pool)
.await?;
@ -232,15 +242,18 @@ 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 ($1, $2, $3, now(), now() + interval '1 day');",
VALUES (?, ?, ?, ?, ?);",
)
.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?;
}
@ -250,12 +263,12 @@ pub async fn google_auth_return(
pub async fn logout(
cookie: Option<TypedHeader<Cookie>>,
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
) -> Result<impl IntoResponse, AppError> {
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 = $1")
let _ = sqlx::query("DELETE FROM user_sessions WHERE session_token_1 = ?")
.bind(session_token[0])
.execute(&db_pool)
.await;

View File

@ -5,11 +5,9 @@ use axum::{
};
use secret_gift_exchange::{giftexchange, giftexchange_save, giftexchanges};
use sqlx::migrate::Migrator;
use sqlx::{PgPool, postgres::PgPoolOptions};
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
use std::net::SocketAddr;
use tower_http::services::ServeDir;
use dotenvy::var;
mod calendar;
mod email;
@ -32,7 +30,7 @@ use wishlist::{user_wishlist, user_wishlist_add, user_wishlist_add_item, user_wi
#[derive(Clone)]
pub struct AppState {
pub db_pool: PgPool,
pub db_pool: SqlitePool,
}
#[tokio::main]
@ -40,12 +38,13 @@ async fn main() {
// initialize tracing
tracing_subscriber::fmt::init();
// 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 db_pool = SqlitePoolOptions::new()
.max_connections(5)
.connect("sqlite://db/db.sqlite3")
.await;
let app_state = AppState {
db_pool: db_pool,
db_pool: db_pool.expect("Failed to get db_pool"),
};
static MIGRATOR: Migrator = sqlx::migrate!();
@ -73,7 +72,7 @@ async fn main() {
.route("/getevents/:calendar", get(get_events))
// Wishlist
.route("/wishlists", get(wishlists))
.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))

View File

@ -1,14 +1,20 @@
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::PgPool;
use sqlx::SqlitePool;
pub async fn inject_user_data(
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
cookie: Option<TypedHeader<Cookie>>,
mut request: Request<Body>,
next: Next,
@ -16,8 +22,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<(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"#,
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=?"#,
)
.bind(session_token[0])
.fetch_one(&db_pool)
@ -38,14 +44,14 @@ pub async fn inject_user_data(
) {
let id = query.0;
let expires_at = query.1;
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)
if expires_at > Utc::now().timestamp() {
let row = sqlx::query_as!(
UserData,
"SELECT * FROM users WHERE id = ?",
id
)
.fetch_one(&db_pool)
.await
.unwrap();
.await?;
request.extensions_mut().insert(Some(UserData {
id: row.id,
@ -56,7 +62,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
}));
}
}
@ -69,9 +75,9 @@ pub async fn inject_user_data(
Ok(next.run(request).await)
}
pub async fn is_authorized(path: &str, user_data: Option<UserData>, db_pool: PgPool) -> bool {
pub async fn is_authorized(path: &str, user_data: Option<UserData>, db_pool: SqlitePool) -> bool {
if let Some(user_data) = user_data {
let query: Result<(uuid::Uuid,), _> = match path {
let query: Result<(i64,), _> = match path {
"/profile" => {
return true;
}
@ -79,11 +85,10 @@ pub async fn is_authorized(path: &str, user_data: Option<UserData>, db_pool: PgP
// 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 = $1"#)
.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 = ?"#)
.bind(remaining_path.to_str().unwrap())
.fetch_one(&db_pool)
.await;
if let Ok(query) = query {
if query.0 != "" {
break;
@ -94,7 +99,7 @@ pub async fn is_authorized(path: &str, user_data: Option<UserData>, db_pool: PgP
}
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 = $1 and email = $2"#)
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 = ?"#)
.bind(remaining_path.to_str().unwrap())
.bind(user_data.email.as_str())
.fetch_one(&db_pool)

View File

@ -5,7 +5,7 @@ use axum::{
Extension,
};
use http::StatusCode;
use sqlx::PgPool;
use sqlx::SqlitePool;
use crate::{
middlewares::is_authorized,
@ -67,7 +67,7 @@ struct DashboardTemplate {
}
pub async fn index(
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
// Is the user logged in?
@ -93,7 +93,7 @@ pub async fn index(
}
pub async fn dashboard(
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> 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<PgPool>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
// Is the user logged in?
@ -158,8 +158,8 @@ pub async fn profile(
}
pub async fn user_profile(
Path(user_id): Path<uuid::Uuid>,
State(db_pool): State<PgPool>,
Path(user_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
// Is the user logged in?
@ -171,8 +171,7 @@ 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( "SELECT * FROM users WHERE id = $1")
.bind(user_id)
let profile = sqlx::query_as!(UserData, "SELECT * FROM users WHERE id = ?", user_id)
.fetch_one(&db_pool)
.await
.unwrap();
@ -216,7 +215,7 @@ struct UserAdminTemplate {
pub async fn useradmin(
Extension(user_data): Extension<Option<UserData>>,
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
) -> impl IntoResponse {
// Is the user logged in?
let logged_in = user_data.is_some();
@ -262,7 +261,17 @@ pub async fn about(Extension(user_data): Extension<Option<UserData>>) -> impl In
let logged_in = user_data.is_some();
// Set empty user
let mut user = UserData::default();
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(),
};
if logged_in {
// Extract the user data.
@ -285,7 +294,17 @@ pub async fn contact(Extension(user_data): Extension<Option<UserData>>) -> impl
let logged_in = user_data.is_some();
// Set empty user
let mut user = UserData::default();
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(),
};
if logged_in {
// Extract the user data.

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use askama::Template;
use askama_axum::{IntoResponse, Response};
use axum::{
@ -7,9 +9,10 @@ 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, PgPool};
use sqlx::{FromRow, SqlitePool};
use crate::{
middlewares::is_authorized,
@ -96,7 +99,7 @@ struct GiftExchangesTemplate {
pub async fn giftexchanges(
Extension(user_data): Extension<Option<UserData>>,
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
) -> impl IntoResponse {
// Is the user logged in?
let logged_in = user_data.is_some();
@ -156,7 +159,7 @@ struct GiftExchangeTemplate {
pub async fn giftexchange(
Path(exchange_id): Path<i64>,
Extension(user_data): Extension<Option<UserData>>,
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
) -> impl IntoResponse {
// Is the user logged in?
let logged_in = user_data.is_some();
@ -171,9 +174,11 @@ 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(
"SELECT * FROM gift_exchange WHERE id = ?")
.bind(exchange_id)
let giftexchange = match sqlx::query_as!(
GiftExchange,
"SELECT * FROM gift_exchange WHERE id = ?",
exchange_id
)
.fetch_one(&db_pool)
.await
{
@ -183,7 +188,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 = $1)",
"select * from users where users.id in (select participant_id from gift_exchange_participants where exchange_id = ?)",
)
.bind(exchange_id)
.fetch_all(&db_pool)
@ -192,7 +197,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 = $1)",
"select * from users where users.id not in (select participant_id from gift_exchange_participants where exchange_id = ?)",
)
.bind(exchange_id)
.fetch_all(&db_pool)
@ -220,15 +225,15 @@ pub async fn giftexchange(
pub struct ExchangeForm {
name: String,
exchange_date: String,
non_participants: Vec<uuid::Uuid>,
non_participants: Vec<i64>,
}
pub async fn giftexchange_save(
State(_db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
request: Request<Body>,
) -> 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") {

View File

@ -4,20 +4,21 @@ use axum::{
response::Redirect,
Extension,
};
use chrono::Utc;
///User related structs and functions
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};
use sqlx::{FromRow, SqlitePool};
use crate::middlewares::is_authorized;
#[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)]
pub struct UserData {
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 id: i64,
pub created_at: i64,
pub created_by: i64,
pub updated_at: i64,
pub updated_by: i64,
pub email: String,
pub name: String,
pub family_name: String,
@ -26,58 +27,58 @@ pub struct UserData {
#[derive(Serialize, Deserialize)]
pub struct RoleData {
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 id: i64,
pub created_at: i64,
pub created_by: i64,
pub updated_at: i64,
pub updated_by: i64,
pub name: String,
pub description: String,
}
#[derive(Default, Clone, Debug, Serialize, Deserialize)]
pub struct UserRoles {
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,
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,
}
#[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)]
pub struct UserRolesDisplay {
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 id: i64,
pub created_at: i64,
pub created_by: i64,
pub updated_at: i64,
pub updated_by: i64,
pub user_id: i64,
pub user_name: String,
pub role_id: uuid::Uuid,
pub role_id: i64,
pub role_name: String,
}
#[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)]
pub struct UserWishlistItem {
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 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: Option<uuid::Uuid>,
pub received_at: Option<chrono::NaiveDateTime>,
pub purchased_by: i64,
pub received_at: i64,
}
/*
pub async fn get_user_roles(user_id: i64, db_pool: &PgPool) -> Vec<UserRoles> {
pub async fn get_user_roles(user_id: i64, db_pool: &SqlitePool) -> Vec<UserRoles> {
// 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 = $1"#
r#"SELECT id, created_at, created_by, updated_at, updated_by, user_id, role_id FROM user_roles WHERE user_id = ?"#
)
.bind(user_id)
.fetch_all(db_pool)
@ -87,10 +88,10 @@ pub async fn get_user_roles(user_id: i64, db_pool: &PgPool) -> Vec<UserRoles> {
user_roles
} */
pub async fn get_user_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> Vec<UserRolesDisplay> {
pub async fn get_user_roles_display(user_id: i64, db_pool: &SqlitePool) -> Vec<UserRolesDisplay> {
// 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 = $1"#
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 = ?"#
)
.bind(user_id)
.fetch_all(db_pool)
@ -100,10 +101,10 @@ pub async fn get_user_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> Ve
user_roles
}
pub async fn get_other_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> Vec<UserRolesDisplay> {
pub async fn get_other_roles_display(user_id: i64, db_pool: &SqlitePool) -> Vec<UserRolesDisplay> {
// Get roles user does not have
let user_roles = sqlx::query_as(
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)"#
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 = ?)"#
)
.bind(user_id.clone())
.bind(user_id)
@ -115,13 +116,17 @@ pub async fn get_other_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> V
}
pub async fn add_user_role(
Path((user_id, role_id)): Path<(uuid::Uuid, uuid::Uuid)>,
State(db_pool): State<PgPool>,
Path((user_id, role_id)): Path<(i64, i64)>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
if is_authorized("/roles", user_data.clone(), db_pool.clone()).await {
sqlx::query("INSERT INTO user_roles (created_by, updated_by, user_id, role_id) VALUES ($1, $2, $3, $4)")
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
.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)
@ -139,12 +144,12 @@ pub async fn add_user_role(
}
pub async fn delete_user_role(
Path((user_id, user_role_id)): Path<(uuid::Uuid, uuid::Uuid)>,
State(db_pool): State<PgPool>,
Path((user_id, user_role_id)): Path<(i64, i64)>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
if is_authorized("/roles", user_data, db_pool.clone()).await {
sqlx::query("DELETE FROM user_roles WHERE id = $1")
sqlx::query("DELETE FROM user_roles WHERE id = ?")
.bind(user_role_id)
.execute(&db_pool)
.await
@ -157,11 +162,11 @@ pub async fn delete_user_role(
}
}
pub async fn get_user_wishlist_item_by_id(item_id: uuid::Uuid, db_pool: &PgPool) -> UserWishlistItem {
pub async fn get_user_wishlist_item_by_id(item_id: i64, db_pool: &SqlitePool) -> 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 = $1"#
from wishlist_items where id = ?"#
)
.bind(item_id)
.fetch_one(db_pool)
@ -171,11 +176,11 @@ pub async fn get_user_wishlist_item_by_id(item_id: uuid::Uuid, db_pool: &PgPool)
user_wishlist_item
}
pub async fn get_user_wishlist_items(user_id: uuid::Uuid, db_pool: &PgPool) -> Vec<UserWishlistItem> {
pub async fn get_user_wishlist_items(user_id: i64, db_pool: &SqlitePool) -> Vec<UserWishlistItem> {
// 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 = $1"#
from wishlist_items where user_id = ?"#
)
.bind(user_id)
.fetch_all(db_pool)
@ -185,8 +190,8 @@ pub async fn get_user_wishlist_items(user_id: uuid::Uuid, db_pool: &PgPool) -> V
user_wishlist_items
}
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"#)
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 = ?"#)
.bind(role_name)
.fetch_one(db_pool)
.await

View File

@ -8,8 +8,7 @@ use axum_extra::response::Html;
use chrono::Utc;
use http::StatusCode;
use serde::Deserialize;
use sqlx::{Row, PgPool};
use uuid::Uuid;
use sqlx::{Row, SqlitePool};
use crate::{
middlewares::is_authorized,
@ -48,7 +47,7 @@ struct WishListsTemplate {
pub async fn wishlists(
Extension(user_data): Extension<Option<UserData>>,
State(db_pool): State<PgPool>,
State(db_pool): State<SqlitePool>,
) -> impl IntoResponse {
// Is the user logged in?
let logged_in = user_data.is_some();
@ -94,8 +93,8 @@ struct UserWishListTemplate {
}
pub async fn user_wishlist(
Path(user_id): Path<uuid::Uuid>,
State(db_pool): State<PgPool>,
Path(user_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
// Is the user logged in?
@ -107,15 +106,14 @@ 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("SELECT * FROM users WHERE id = $1")
.bind(user_id)
let person = 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_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;
@ -152,8 +150,8 @@ struct UserWishListAddTemplate {
}
pub async fn user_wishlist_add(
Path(user_id): Path<uuid::Uuid>,
State(db_pool): State<PgPool>,
Path(user_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
// Is the user logged in?
@ -165,8 +163,7 @@ 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("SELECT * FROM users WHERE id = $1")
.bind(user_id)
let person = sqlx::query_as!(UserData, "SELECT * FROM users WHERE id = ?", user_id)
.fetch_one(&db_pool)
.await
.unwrap();
@ -202,15 +199,19 @@ pub struct ItemForm {
}
pub async fn user_wishlist_add_item(
Path(user_id): Path<uuid::Uuid>,
State(db_pool): State<PgPool>,
Path(user_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
Form(item_form): Form<ItemForm>,
) -> impl IntoResponse {
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
// Insert new item to database
sqlx::query("insert into wishlist_items (created_by, updated_by, user_id, item, item_url) values ($1, $2, $3, $4, $5)")
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)
@ -236,8 +237,8 @@ struct UserWishListEditTemplate {
}
pub async fn user_wishlist_edit_item(
Path(item_id): Path<uuid::Uuid>,
State(db_pool): State<PgPool>,
Path(item_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
// Is the user logged in?
@ -273,16 +274,16 @@ pub async fn user_wishlist_edit_item(
}
pub async fn user_wishlist_save_item(
Path(item_id): Path<uuid::Uuid>,
State(db_pool): State<PgPool>,
Path(item_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
Form(item_form): Form<ItemForm>,
) -> impl IntoResponse {
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
// Insert new item to database
let now = Utc::now().naive_local();
let now = Utc::now().timestamp();
sqlx::query("update wishlist_items set updated_at = $1, updated_by = $2, item = $3, item_url = $4 where id = $5")
sqlx::query("update wishlist_items set updated_at = ?, updated_by = ?, item = ?, item_url = ? where id = ?")
.bind(now) // Updated now
.bind(user_data.as_ref().unwrap().id) // Updated by current user
.bind(item_form.item)
@ -301,28 +302,28 @@ pub async fn user_wishlist_save_item(
}
pub async fn user_wishlist_bought_item(
Path(item_id): Path<uuid::Uuid>,
State(db_pool): State<PgPool>,
Path(user_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> 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 = $1 where id = $2")
sqlx::query("update wishlist_items set purchased_by = ? where id = ?")
.bind(user_data.as_ref().unwrap().id) // Created by current user
.bind(item_id)
.bind(user_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 = $1")
.bind(item_id)
let row = sqlx::query("SELECT user_id FROM wishlist_items WHERE id = ?")
.bind(user_id)
.fetch_one(&db_pool)
.await
.unwrap();
let userid = row.get::<uuid::Uuid, _>("user_id");
let userid = row.get::<i64, _>("user_id");
let redirect_string = format!("/userwishlist/{userid}");
Redirect::to(&redirect_string).into_response()
} else {
@ -331,15 +332,15 @@ pub async fn user_wishlist_bought_item(
}
pub async fn user_wishlist_received_item(
Path(user_id): Path<uuid::Uuid>,
State(db_pool): State<PgPool>,
Path(user_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
// Update item received time
let now = Utc::now().naive_local();
let now = Utc::now().timestamp();
sqlx::query("update wishlist_items set received_at = $1 where id = $2")
sqlx::query("update wishlist_items set received_at = ? where id = ?")
.bind(now) // Received now
.bind(user_id)
.execute(&db_pool)
@ -356,12 +357,12 @@ pub async fn user_wishlist_received_item(
}
pub async fn user_wishlist_delete_item(
Path(item_id): Path<uuid::Uuid>,
State(db_pool): State<PgPool>,
Path(item_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
sqlx::query("delete from wishlist_items where id = $1")
sqlx::query("delete from wishlist_items where id = ?")
.bind(item_id)
.execute(&db_pool)
.await
@ -377,12 +378,12 @@ pub async fn user_wishlist_delete_item(
}
pub async fn user_wishlist_returned_item(
Path(item_id): Path<uuid::Uuid>,
State(db_pool): State<PgPool>,
Path(item_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
sqlx::query("update wishlist_items set purchased_by = null where id = $1")
sqlx::query("update wishlist_items set purchased_by = 0 where id = ?")
.bind(item_id)
.execute(&db_pool)
.await
@ -390,13 +391,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 = $1")
let row = sqlx::query("SELECT user_id FROM wishlist_items WHERE id = ?")
.bind(item_id)
.fetch_one(&db_pool)
.await
.unwrap();
let profileid = row.get::<Uuid, _>("user_id");
let profileid = row.get::<i64, _>("user_id");
let redirect_string = format!("/userwishlist/{profileid}");
Redirect::to(&redirect_string).into_response()
} else {

View File

@ -25,44 +25,38 @@
{% for person_wishlist_item in person_wishlist_items %}
<tr>
{% if my_wishlist %}
<td><a href="/userwishlist/edit/{{ person_wishlist_item.id }}">{{ person_wishlist_item.item }}</a></td>
<td><a href="/userwishlist/edit/{{ person_wishlist_item.id }}">{{ person_wishlist_item.item }}</a></td>
{% else %}
<td>{{ person_wishlist_item.item }}</td>
<td>{{ person_wishlist_item.item }}</td>
{% endif %}
{% if person_wishlist_item.item_url.len() > 0 %}
<td><a href="{{ person_wishlist_item.item_url }}">URL</a></td>
<td><a href="{{ person_wishlist_item.item_url }}">URL</a></td>
{% else %}
<td></td>
<td></td>
{% endif %}
{% match person_wishlist_item.received_at %}
{% when None %}
<td>Not yet!</td>
{% when Some with (received_at) %}
<td>Got it!</td>
{% endmatch %}
{% if person_wishlist_item.received_at > 0 %}
<td>Got it!</td>
{% else %}
<td>Not yet!</td>
{% endif %}
{% if my_wishlist %}
{% match person_wishlist_item.received_at %}
{% when None %}
<td><a href="/userwishlist/received/{{ person_wishlist_item.id }}">Received</a></td>
{% when Some with (received_at) %}
<td><a href="/userwishlist/delete/{{ person_wishlist_item.id }}">Delete</a></td>
{% endmatch %}
{% if person_wishlist_item.received_at > 0 %}
<td><a href="/userwishlist/delete/{{ person_wishlist_item.id }}">Delete</a></td>
{% else %}
<td><a href="/userwishlist/received/{{ person_wishlist_item.id }}">Received</a></td>
{% endif %}
{% else %}
{% match person_wishlist_item.purchased_by %}
{% when Some with (purchased_by) %}
{% if purchased_by.clone() == user.id %}
<td><a href="/userwishlist/returned/{{ person_wishlist_item.id }}">Return</a></td>
{% else %}
<td>Purchased</td>
{% endif %}
{% when None %}
<td><a href="/userwishlist/bought/{{ person_wishlist_item.id }}">Bought</a></td>
{% endmatch %}
{% if person_wishlist_item.purchased_by == user.id %}
<td><a href="/userwishlist/returned/{{ person_wishlist_item.id }}">Return</a></td>
{% else if person_wishlist_item.purchased_by > 0 %}
<td>Purchased</td>
{% else %}
<td><a href="/userwishlist/bought/{{ person_wishlist_item.id }}">Bought</a></td>
{% endif %}
{% endif %}
</tr>
{% endfor %}
</tbody>