Compare commits
17 Commits
462716633e
...
50bc1f4eb5
| Author | SHA1 | Date |
|---|---|---|
|
|
50bc1f4eb5 | |
|
|
e59f849abd | |
|
|
d156cbfa98 | |
|
|
fe8056179b | |
|
|
904cd4315c | |
|
|
e9edd3e82a | |
|
|
8032019807 | |
|
|
682ea672c4 | |
|
|
f7e643ffbd | |
|
|
fd2ec59d33 | |
|
|
c5100f31e5 | |
|
|
d09b280061 | |
|
|
6bdf37e91a | |
|
|
6552b1248f | |
|
|
a8ca9bfafe | |
|
|
1589ebfd37 | |
|
|
c9dd17ae14 |
|
|
@ -1,4 +1,4 @@
|
||||||
FROM mcr.microsoft.com/devcontainers/rust:1-1-bookworm
|
FROM mcr.microsoft.com/devcontainers/rust:latest
|
||||||
|
|
||||||
# Include lld linker to improve build times either by using environment variable
|
# Include lld linker to improve build times either by using environment variable
|
||||||
# RUSTFLAGS="-C link-arg=-fuse-ld=lld" or with Cargo's configuration file (i.e see .cargo/config.toml).
|
# RUSTFLAGS="-C link-arg=-fuse-ld=lld" or with Cargo's configuration file (i.e see .cargo/config.toml).
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,6 @@ backend/id_rsa
|
||||||
backend/id_rsa.pub
|
backend/id_rsa.pub
|
||||||
backend/sqlite3.env
|
backend/sqlite3.env
|
||||||
backend/.env
|
backend/.env
|
||||||
|
backend/prod.env
|
||||||
|
backend/test.env
|
||||||
|
backend/dev.env
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,6 @@
|
||||||
"now.instance.host.url": "",
|
"now.instance.host.url": "",
|
||||||
"now.instance.OAuth.client.id": "",
|
"now.instance.OAuth.client.id": "",
|
||||||
"now.instance.user.password": "",
|
"now.instance.user.password": "",
|
||||||
"now.instance.OAuth.client.secret": ""
|
"now.instance.OAuth.client.secret": "",
|
||||||
|
"postman.settings.dotenv-detection-notification-visibility": false
|
||||||
}
|
}
|
||||||
35
README.md
35
README.md
|
|
@ -1,3 +1,38 @@
|
||||||
# Jean-Marie family website
|
# Jean-Marie family website
|
||||||
|
|
||||||
The Jean-Marie family website is a place to share information about family activities
|
The Jean-Marie family website is a place to share information about family activities
|
||||||
|
|
||||||
|
Postgresql server setup and configuration
|
||||||
|
- Create new debian container
|
||||||
|
- 2GB RAM
|
||||||
|
- 8G Storage
|
||||||
|
- link to ZFS storage pool
|
||||||
|
- Update packages
|
||||||
|
- apt update && apt upgrade -y && apt autoremove -y
|
||||||
|
- Install current postgresql source
|
||||||
|
- apt install -y postgresql-common
|
||||||
|
- /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
|
||||||
|
- apt install -y postgresql
|
||||||
|
- Move cluster to ZFS
|
||||||
|
- Stop postgresql
|
||||||
|
- systemctl stop postgresql
|
||||||
|
- Create new directory for cluster in ZFS
|
||||||
|
- from ZFS server create world writable directory
|
||||||
|
- Move current cluster to new location
|
||||||
|
- from ZFS server ensure ownership of all files in moved directory is postgres
|
||||||
|
- Point to new location
|
||||||
|
- nano /etc/postgresql/<version>/main/postgresql.conf
|
||||||
|
- data_directory = '<new location>'
|
||||||
|
- Restart postgresql
|
||||||
|
- systemctl start postgresql
|
||||||
|
- Allow external access
|
||||||
|
- Edit pg_hba.conf
|
||||||
|
- Change host all address to local network
|
||||||
|
- Edit postgresql.conf
|
||||||
|
- Change listen_adress to '*'
|
||||||
|
- Change password
|
||||||
|
- su postgres
|
||||||
|
- psql
|
||||||
|
- alter user postgres with password '<new password>';
|
||||||
|
- \q
|
||||||
|
- exit
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,10 +7,10 @@ edition = "2021"
|
||||||
|
|
||||||
# Update all dependencies with `cargo upgrade -i allow && cargo update`
|
# Update all dependencies with `cargo upgrade -i allow && cargo update`
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.7.6" }
|
axum = { version = "0.8.1" }
|
||||||
axum_session = { version = "0.14.2" }
|
axum_session = { version = "0.16.0" }
|
||||||
axum-server = { version = "0.7.1" }
|
axum-server = { version = "0.7.2" }
|
||||||
axum-extra = { version = "0.9.4", features = ["cookie-private", "typed-header"] }
|
axum-extra = { version = "0.10.0", features = ["cookie-private", "typed-header"] }
|
||||||
askama = "0.12.0"
|
askama = "0.12.0"
|
||||||
askama_axum = "0.4.0"
|
askama_axum = "0.4.0"
|
||||||
headers = "0.4"
|
headers = "0.4"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- Remove person level tables
|
||||||
|
ALTER TABLE if exists users
|
||||||
|
drop column if exists person_id;
|
||||||
|
|
||||||
|
drop table if exists people;
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
-- Add login table
|
||||||
|
create table if not exists people (
|
||||||
|
id uuid NOT NULL DEFAULT gen_random_uuid (),
|
||||||
|
created_at timestamp without time zone NOT NULL DEFAULT now(),
|
||||||
|
created_by uuid NOT NULL,
|
||||||
|
updated_at timestamp without time zone 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,
|
||||||
|
CONSTRAINT people_pkey PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE if exists users
|
||||||
|
ADD COLUMN IF NOT EXISTS person_id uuid REFERENCES people (id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Copy accounts(users) to profiles(people)
|
||||||
|
insert into people (created_by, updated_by, email, name, family_name, given_name)
|
||||||
|
select created_by, updated_by, email, name, family_name, given_name from users
|
||||||
|
on conflict do nothing;
|
||||||
|
|
||||||
|
-- Link accounts to profiles
|
||||||
|
update users u set person_id = p.id from people p where p.email = u.email;
|
||||||
|
|
||||||
|
-- Move wishlist items from accounts to profiles
|
||||||
|
update wishlist_items wi set user_id = p.person_id from users p where p.id = wi.user_id;
|
||||||
|
|
||||||
|
-- Copy normal role from accounts to profiles
|
||||||
|
insert into user_roles (created_at, created_by, updated_at, updated_by, user_id, role_id)
|
||||||
|
select ur.created_at, ur.created_by, ur.updated_at, ur.updated_by, u.person_id, ur.role_id from user_roles ur join roles r on r.id = ur.role_id join users u on u.id = ur.user_id where r.name = 'normal'
|
||||||
|
on conflict do nothing;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- Remove column from calendar_events
|
||||||
|
ALTER TABLE if exists calendar_events
|
||||||
|
drop column if exists state;
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- Add column to calendar_events
|
||||||
|
ALTER TABLE if exists calendar_events
|
||||||
|
ADD COLUMN IF NOT EXISTS state character varying(25);
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
-- Remove default calendar and event types
|
||||||
|
DELETE FROM calendar WHERE name = 'Cottage';
|
||||||
|
DELETE FROM calendar WHERE name = 'Family tree';
|
||||||
|
|
||||||
|
DELETE FROM calendar_event_types WHERE name = 'Reservation';
|
||||||
|
DELETE FROM calendar_event_types WHERE name = 'Life event';
|
||||||
|
|
||||||
|
-- Remove constraints
|
||||||
|
ALTER TABLE public.calendar DROP CONSTRAINT unique_name;
|
||||||
|
ALTER TABLE public.calendar_event_types DROP CONSTRAINT unique_name;
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
-- Add required constraints
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Check if the constraint 'unique_name' already exists
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'unique_calendar_name'
|
||||||
|
AND conrelid = 'public.calendar'::regclass -- Specify table to narrow down
|
||||||
|
) THEN
|
||||||
|
-- If it doesn't exist, add the constraint
|
||||||
|
ALTER TABLE IF EXISTS public.calendar
|
||||||
|
ADD CONSTRAINT unique_calendar_name UNIQUE (name);
|
||||||
|
RAISE NOTICE 'Constraint unique_calendar_name added to table calendar.';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'Constraint unique_calendar_name already exists on table calendar.';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Check if the constraint 'unique_name' already exists
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'unique_calendar_et_name'
|
||||||
|
AND conrelid = 'public.calendar_event_types'::regclass -- Specify table to narrow down
|
||||||
|
) THEN
|
||||||
|
-- If it doesn't exist, add the constraint
|
||||||
|
ALTER TABLE IF EXISTS public.calendar_event_types
|
||||||
|
ADD CONSTRAINT unique_calendar_et_name UNIQUE (name);
|
||||||
|
RAISE NOTICE 'Constraint unique_calendar_et_name added to table calendar_event_types.';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'Constraint unique_calendar_et_name already exists on table calendar_event_types.';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Add default calendar and event types
|
||||||
|
insert into calendar (created_by, updated_by, name)
|
||||||
|
select id, id, 'Cottage' from users where email = 'admin@jean-marie.ca'
|
||||||
|
on conflict (name) do nothing;
|
||||||
|
|
||||||
|
insert into calendar (created_by, updated_by, name)
|
||||||
|
select id, id, 'Family tree' from users where email = 'admin@jean-marie.ca'
|
||||||
|
on conflict (name) do nothing;
|
||||||
|
|
||||||
|
insert into calendar_event_types (created_by, updated_by, name)
|
||||||
|
select id, id, 'Reservation' from users where email = 'admin@jean-marie.ca'
|
||||||
|
on conflict (name) do nothing;
|
||||||
|
|
||||||
|
insert into calendar_event_types (created_by, updated_by, name)
|
||||||
|
select id, id, 'Life event' from users where email = 'admin@jean-marie.ca'
|
||||||
|
on conflict (name) do nothing;
|
||||||
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Add down migration script here
|
||||||
|
ALTER TABLE if exists calendar
|
||||||
|
drop column if exists colour;
|
||||||
|
|
||||||
|
ALTER TABLE if exists calendar_event_types
|
||||||
|
drop column if exists colour;
|
||||||
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Add up migration script here
|
||||||
|
ALTER TABLE if exists calendar
|
||||||
|
ADD COLUMN IF NOT EXISTS colour character varying(50);
|
||||||
|
|
||||||
|
ALTER TABLE if exists calendar_event_types
|
||||||
|
ADD COLUMN IF NOT EXISTS colour character varying(50);
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
-- Add down migration script here
|
||||||
|
ALTER TABLE if exists calendar_event_types
|
||||||
|
drop column if exists state;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Check if the constraint 'unique_calendar_et_name' already exists
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'unique_calendar_et_name'
|
||||||
|
AND conrelid = 'public.calendar_event_types'::regclass -- Specify table to narrow down
|
||||||
|
) THEN
|
||||||
|
-- If it exists, drop the constraint
|
||||||
|
ALTER TABLE IF EXISTS public.calendar_event_types
|
||||||
|
DROP CONSTRAINT unique_calendar_et_name;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Constraint unique_calendar_et_name dropped from table calendar_event_types.';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'Constraint unique_calendar_et_name doesn''t exist on table calendar_event_types.';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Add the constraint
|
||||||
|
ALTER TABLE IF EXISTS public.calendar_event_types
|
||||||
|
ADD CONSTRAINT unique_calendar_et_name UNIQUE (name);
|
||||||
|
|
||||||
|
RAISE NOTICE 'Constraint unique_calendar_et_name added to table calendar_event_types.';
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
-- Add up migration script here
|
||||||
|
ALTER TABLE if exists calendar_event_types
|
||||||
|
ADD COLUMN IF NOT EXISTS state character varying(25);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Check if the constraint 'unique_calendar_et_name' already exists
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'unique_calendar_et_name'
|
||||||
|
AND conrelid = 'public.calendar_event_types'::regclass -- Specify table to narrow down
|
||||||
|
) THEN
|
||||||
|
-- If it exists, drop the constraint
|
||||||
|
ALTER TABLE IF EXISTS public.calendar_event_types
|
||||||
|
DROP CONSTRAINT unique_calendar_et_name;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Add the constraint
|
||||||
|
ALTER TABLE IF EXISTS public.calendar_event_types
|
||||||
|
ADD CONSTRAINT unique_calendar_et_name UNIQUE (name, state);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
-- Add down migration script here
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
-- Truncate tables
|
||||||
|
TRUNCATE TABLE calendar_event_types, calendar;
|
||||||
|
|
||||||
|
-- Add default calendar and event types
|
||||||
|
insert into calendar (created_by, updated_by, name, colour)
|
||||||
|
select id, id, 'Cottage', 'blue' from users where email = 'admin@jean-marie.ca';
|
||||||
|
|
||||||
|
insert into calendar (created_by, updated_by, name, colour)
|
||||||
|
select id, id, 'Family tree', 'brown' from users where email = 'admin@jean-marie.ca';
|
||||||
|
|
||||||
|
insert into calendar_event_types (created_by, updated_by, name, state, colour)
|
||||||
|
select id, id, 'Reservation', 'Requested', 'purple' from users where email = 'admin@jean-marie.ca';
|
||||||
|
|
||||||
|
insert into calendar_event_types (created_by, updated_by, name, state, colour)
|
||||||
|
select id, id, 'Reservation', 'Approved', 'green' from users where email = 'admin@jean-marie.ca';
|
||||||
|
|
||||||
|
insert into calendar_event_types (created_by, updated_by, name, state, colour)
|
||||||
|
select id, id, 'Reservation', 'Confirmed', 'blue' from users where email = 'admin@jean-marie.ca';
|
||||||
|
|
||||||
|
insert into calendar_event_types (created_by, updated_by, name, state, colour)
|
||||||
|
select id, id, 'Reservation', 'Tentative', 'light-purple' from users where email = 'admin@jean-marie.ca';
|
||||||
|
|
||||||
|
insert into calendar_event_types (created_by, updated_by, name, state, colour)
|
||||||
|
select id, id, 'Reservation', 'Rejected', 'red' from users where email = 'admin@jean-marie.ca';
|
||||||
|
|
||||||
|
insert into calendar_event_types (created_by, updated_by, name, state, colour)
|
||||||
|
select id, id, 'Reservation', 'Cancelled', 'light-red' from users where email = 'admin@jean-marie.ca';
|
||||||
|
|
||||||
|
insert into calendar_event_types (created_by, updated_by, name, state, colour)
|
||||||
|
select id, id, 'Life event', 'Birthday', 'green' from users where email = 'admin@jean-marie.ca';
|
||||||
|
|
||||||
|
insert into calendar_event_types (created_by, updated_by, name, state, colour)
|
||||||
|
select id, id, 'Life event', 'Anniversary', 'orange' from users where email = 'admin@jean-marie.ca';
|
||||||
|
|
||||||
|
insert into calendar_event_types (created_by, updated_by, name, state, colour)
|
||||||
|
select id, id, 'Life event', 'Other', 'light-orange' from users where email = 'admin@jean-marie.ca';
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Delete old permissions
|
||||||
|
delete from role_permissions where item like 'calendar%';
|
||||||
|
|
||||||
|
-- Remove requester column
|
||||||
|
ALTER TABLE if exists calendar_events
|
||||||
|
DROP COLUMN IF EXISTS requester_id uuid;
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- Add requester_id to calendar_events
|
||||||
|
ALTER TABLE if exists calendar_events
|
||||||
|
ADD COLUMN IF NOT EXISTS requester_id uuid;
|
||||||
|
|
||||||
|
-- Copy created_by to requester_id for all current records
|
||||||
|
UPDATE calendar_events
|
||||||
|
SET requester_id = created_by;
|
||||||
|
|
||||||
|
-- Add rbac data
|
||||||
|
insert into role_permissions (item, created_by, updated_by, role_id)
|
||||||
|
select 'calendar:personal:*', u.id, u.id, r.id from users u, roles r where email = 'admin@jean-marie.ca' and r.name = 'normal';
|
||||||
|
|
||||||
|
insert into role_permissions (item, created_by, updated_by, role_id)
|
||||||
|
select 'calendar:read:*', u.id, u.id, r.id from users u, roles r where email = 'admin@jean-marie.ca' and r.name = 'calendar';
|
||||||
|
|
||||||
|
insert into role_permissions (item, created_by, updated_by, role_id)
|
||||||
|
select 'calendar:admin:*', u.id, u.id, r.id from users u, roles r where email = 'admin@jean-marie.ca' and r.name = 'admin';
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_axum::{IntoResponse, Response};
|
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body, extract::{Path, Query, Request, State}, response::{Html, Redirect}, Extension
|
extract::{Query, State},
|
||||||
|
response::{Html, IntoResponse, Redirect, Response},
|
||||||
|
Extension, Form,
|
||||||
};
|
};
|
||||||
|
use chrono::{Days, TimeDelta};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
|
use rbac::RbacService;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::Value;
|
||||||
use sqlx::{types::Json, PgPool, Row};
|
use sqlx::{postgres::PgRow, Error, FromRow, PgPool, Row};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
middlewares::is_authorized,
|
middlewares::is_authorized,
|
||||||
user::{get_user_roles_display, UserData},
|
rbac,
|
||||||
|
user::{get_user_roles_display, AccountData},
|
||||||
};
|
};
|
||||||
|
|
||||||
struct HtmlTemplate<T>(T);
|
struct HtmlTemplate<T>(T);
|
||||||
|
|
@ -31,7 +36,9 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Calendar {
|
#[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct Calendar {
|
||||||
|
id: Uuid,
|
||||||
name: String,
|
name: String,
|
||||||
colour: String,
|
colour: String,
|
||||||
}
|
}
|
||||||
|
|
@ -40,37 +47,43 @@ struct Calendar {
|
||||||
#[template(path = "calendar.html")]
|
#[template(path = "calendar.html")]
|
||||||
struct CalendarTemplate {
|
struct CalendarTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
user_roles: Vec<crate::user::UserRolesDisplay>,
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
calendars: Vec<Calendar>,
|
calendars: Vec<Calendar>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn calendar(
|
pub async fn calendar(
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
Extension(rbac): Extension<RbacService>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
let mut is_authorized = false;
|
||||||
|
|
||||||
if logged_in {
|
if logged_in {
|
||||||
// Extract the user data.
|
// Extract the user data.
|
||||||
let user = user_data.as_ref().unwrap().clone();
|
let user = user_data.as_ref().unwrap().clone();
|
||||||
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||||
|
|
||||||
if is_authorized("/calendar", user_data, db_pool.clone()).await {
|
if !rbac.has_permission(userid, "calendar:admin:*").await {
|
||||||
|
if !rbac.has_permission(userid, "calendar:read:*").await {
|
||||||
|
if !rbac.has_permission(userid, "calendar:personal:*").await {
|
||||||
|
} else {
|
||||||
|
is_authorized = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
is_authorized = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
is_authorized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_authorized {
|
||||||
// Get user roles
|
// Get user roles
|
||||||
let user_roles = get_user_roles_display(userid, &db_pool.clone()).await;
|
let user_roles = get_user_roles_display(userid, &db_pool.clone()).await;
|
||||||
|
|
||||||
let calendars: Vec<Calendar> = vec![
|
let calendars: Vec<Calendar> = get_calendars(db_pool.clone()).await;
|
||||||
Calendar {
|
|
||||||
name: "Cottage".to_string(),
|
|
||||||
colour: "green".to_string(),
|
|
||||||
},
|
|
||||||
Calendar {
|
|
||||||
name: "Personal".to_string(),
|
|
||||||
colour: "blue".to_string(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let template = CalendarTemplate {
|
let template = CalendarTemplate {
|
||||||
logged_in,
|
logged_in,
|
||||||
|
|
@ -92,13 +105,52 @@ pub struct EventParams {
|
||||||
start: String,
|
start: String,
|
||||||
end: String,
|
end: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_calendars(db_pool: PgPool) -> Vec<Calendar> {
|
||||||
|
let calendars: Vec<Calendar> = sqlx::query_as(r#"select id, name, colour from calendar"#)
|
||||||
|
.fetch_all(&db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
calendars
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_event(event_id: Uuid, db_pool: &PgPool) -> String {
|
||||||
|
// Set default events
|
||||||
|
let mut eventstring: String = "[]".to_string();
|
||||||
|
|
||||||
|
let event: Result<PgRow, Error> = Ok(sqlx::query(
|
||||||
|
r#"select to_json(json_build_object(
|
||||||
|
'title', ce.title,
|
||||||
|
'start', ce.start_time,
|
||||||
|
'end', ce.end_time,
|
||||||
|
'allDay', false,
|
||||||
|
'backgroundColor', cet.colour))
|
||||||
|
from calendar_events ce
|
||||||
|
join calendar_event_types cet on cet.id = ce.event_type_id
|
||||||
|
where ce.id = $1"#,
|
||||||
|
)
|
||||||
|
.bind(event_id)
|
||||||
|
.fetch_one(db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap());
|
||||||
|
|
||||||
|
if let Ok(json_string) = event {
|
||||||
|
if let Ok(stringevents) = json_string.try_get_raw(0).map(|v| v.as_str().unwrap_or("")) {
|
||||||
|
//println!("Event: {:?}", stringevents);
|
||||||
|
eventstring = stringevents.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventstring
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_events(
|
pub async fn get_events(
|
||||||
Path(calendar): Path<String>,
|
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Query(params): Query<EventParams>,
|
Query(params): Query<EventParams>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
Extension(rbac): Extension<RbacService>,
|
||||||
) -> String {
|
) -> String {
|
||||||
println!("Calendar: {}", calendar);
|
|
||||||
//println!("Paramters: {:?}", params);
|
//println!("Paramters: {:?}", params);
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
|
@ -106,39 +158,473 @@ pub async fn get_events(
|
||||||
// Set default events
|
// Set default events
|
||||||
let mut eventstring: String = "[]".to_string();
|
let mut eventstring: String = "[]".to_string();
|
||||||
|
|
||||||
|
if logged_in {
|
||||||
|
// User is logged in
|
||||||
|
//println!("User is 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 personid = user_data
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.person_id.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Set empty query string
|
||||||
|
let mut query = r#""#;
|
||||||
|
|
||||||
|
if !rbac.has_permission(userid, "calendar:admin:*").await {
|
||||||
|
if !rbac.has_permission(userid, "calendar:read:*").await {
|
||||||
|
if !rbac.has_permission(userid, "calendar:personal:*").await {
|
||||||
|
} else {
|
||||||
|
query = r#"select to_json(json_agg(jbo.val))
|
||||||
|
from (select json_build_object(
|
||||||
|
'id', ce.id,
|
||||||
|
'title', ce.title,
|
||||||
|
'start', ce.start_time,
|
||||||
|
'end', ce.end_time,
|
||||||
|
'allDay', false,
|
||||||
|
'backgroundColor', cet.colour)
|
||||||
|
from calendar_events ce
|
||||||
|
join calendar c on c.id = ce.calendar_id
|
||||||
|
join calendar_event_types cet on cet.id = ce.event_type_id
|
||||||
|
where ce.celebrate = true
|
||||||
|
and c.name = 'Cottage'
|
||||||
|
and start_time > $1
|
||||||
|
and start_time < $2
|
||||||
|
and ce.created_by = $3
|
||||||
|
union all
|
||||||
|
select json_build_object(
|
||||||
|
'id', ce.id,
|
||||||
|
'title', 'In use',
|
||||||
|
'start', ce.start_time,
|
||||||
|
'end', ce.end_time,
|
||||||
|
'allDay', false,
|
||||||
|
'backgroundColor', cet.colour)
|
||||||
|
from calendar_events ce
|
||||||
|
join calendar c on c.id = ce.calendar_id
|
||||||
|
join calendar_event_types cet on cet.id = ce.event_type_id
|
||||||
|
where ce.celebrate = true
|
||||||
|
and c.name = 'Cottage'
|
||||||
|
and start_time > $1
|
||||||
|
and start_time < $2
|
||||||
|
and ce.created_by != $3) as jbo(val)"#;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query = r#"select to_json(json_agg(json_build_object(
|
||||||
|
'id', ce.id,
|
||||||
|
'title', ce.title,
|
||||||
|
'start', ce.start_time,
|
||||||
|
'end', ce.end_time,
|
||||||
|
'allDay', false,
|
||||||
|
'backgroundColor', cet.colour)))
|
||||||
|
from calendar_events ce
|
||||||
|
join calendar c on c.id = ce.calendar_id
|
||||||
|
join calendar_event_types cet on cet.id = ce.event_type_id
|
||||||
|
where ce.celebrate = true
|
||||||
|
and c.name = 'Cottage'
|
||||||
|
and start_time > $1
|
||||||
|
and start_time < $2"#;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query = r#"select to_json(json_agg(json_build_object(
|
||||||
|
'id', ce.id,
|
||||||
|
'title', ce.title,
|
||||||
|
'start', ce.start_time,
|
||||||
|
'end', ce.end_time,
|
||||||
|
'allDay', false,
|
||||||
|
'backgroundColor', cet.colour)))
|
||||||
|
from calendar_events ce
|
||||||
|
join calendar c on c.id = ce.calendar_id
|
||||||
|
join calendar_event_types cet on cet.id = ce.event_type_id
|
||||||
|
where ce.celebrate = true
|
||||||
|
and c.name = 'Cottage'
|
||||||
|
and start_time > $1
|
||||||
|
and start_time < $2"#;
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.len() > 0 {
|
||||||
|
// User is authorized
|
||||||
|
//println!("User is authorized");
|
||||||
|
|
||||||
|
// Get requested calendar events from database
|
||||||
|
let events = sqlx::query(query)
|
||||||
|
.bind(chrono::DateTime::parse_from_rfc3339(¶ms.start).unwrap())
|
||||||
|
.bind(chrono::DateTime::parse_from_rfc3339(¶ms.end).unwrap())
|
||||||
|
.bind(personid)
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
//println!("Events: {:?}", events);
|
||||||
|
|
||||||
|
if let Ok(json_string) = events {
|
||||||
|
if let Ok(stringevents) =
|
||||||
|
json_string.try_get_raw(0).map(|v| v.as_str().unwrap_or(""))
|
||||||
|
{
|
||||||
|
//println!("PgValue: {:?}", stringevents);
|
||||||
|
eventstring = stringevents.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventstring
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct Event {
|
||||||
|
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 calendar_id: uuid::Uuid,
|
||||||
|
pub event_type_id: uuid::Uuid,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub state: String,
|
||||||
|
pub start_time: chrono::NaiveDateTime,
|
||||||
|
pub end_time: chrono::NaiveDateTime,
|
||||||
|
pub celebrate: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct EventCreate {
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub state: String,
|
||||||
|
pub calendar_id: uuid::Uuid,
|
||||||
|
pub event_type_id: uuid::Uuid,
|
||||||
|
pub start_time: String,
|
||||||
|
pub end_time: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_event(
|
||||||
|
State(db_pool): State<PgPool>,
|
||||||
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
Extension(rbac): Extension<RbacService>,
|
||||||
|
Form(event): Form<EventCreate>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if is_authorized("/calendar", user_data.clone(), db_pool.clone()).await {
|
||||||
|
let fmt = "%Y-%m-%d";
|
||||||
|
let start_date = chrono::NaiveDate::parse_from_str(&event.start_time, fmt).unwrap();
|
||||||
|
let end_date = chrono::NaiveDate::parse_from_str(&event.end_time, fmt)
|
||||||
|
.unwrap()
|
||||||
|
.checked_sub_days(Days::new(1))
|
||||||
|
.unwrap();
|
||||||
|
let start_datetime = start_date.and_hms_opt(14, 0, 0).unwrap();
|
||||||
|
let end_datetime = end_date.and_hms_opt(10, 0, 0).unwrap();
|
||||||
|
|
||||||
|
let start_dt = start_datetime.and_utc();
|
||||||
|
let end_dt = end_datetime.and_utc();
|
||||||
|
|
||||||
|
let _ = sqlx::query(
|
||||||
|
r#"INSERT INTO calendar_events (created_by, updated_by, calendar_id, event_type_id, title, description, state, start_time, end_time)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"#)
|
||||||
|
.bind(user_data.as_ref().unwrap().id)// Created by current user
|
||||||
|
.bind(user_data.as_ref().unwrap().id) // Updated by current user
|
||||||
|
.bind(event.calendar_id)
|
||||||
|
.bind(event.event_type_id)
|
||||||
|
.bind(event.title)
|
||||||
|
.bind(event.description)
|
||||||
|
.bind(event.state)
|
||||||
|
.bind(start_dt)
|
||||||
|
.bind(end_dt)
|
||||||
|
.execute(&db_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, format!("Error creating event: {}", e))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let redirect_url = format!("/calendar");
|
||||||
|
Redirect::to(&redirect_url).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct NewRequest {
|
||||||
|
pub start: String,
|
||||||
|
pub end: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_request(
|
||||||
|
State(db_pool): State<PgPool>,
|
||||||
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
Extension(rbac): Extension<RbacService>,
|
||||||
|
request: axum::http::Request<axum::body::Body>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
// Is the user logged in?
|
||||||
|
let logged_in = user_data.is_some();
|
||||||
|
let mut is_authorized = false;
|
||||||
|
|
||||||
|
// Set default events
|
||||||
|
let mut eventstring: String = "[]".to_string();
|
||||||
|
|
||||||
|
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 personid = user_data
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.person_id.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if !rbac.has_permission(userid, "calendar:admin:*").await {
|
||||||
|
if !rbac.has_permission(userid, "calendar:read:*").await {
|
||||||
|
if !rbac.has_permission(userid, "calendar:personal:*").await {
|
||||||
|
} else {
|
||||||
|
is_authorized = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
is_authorized = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
is_authorized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_authorized {
|
||||||
|
let (_parts, body) = request.into_parts();
|
||||||
|
let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||||
|
let body_str = String::from_utf8(bytes.to_vec()).unwrap();
|
||||||
|
//println!("Body: {}", body_str);
|
||||||
|
|
||||||
|
let params: NewRequest = serde_json::from_str(&body_str).unwrap();
|
||||||
|
|
||||||
|
let fmt = "%Y-%m-%d";
|
||||||
|
let start_date = chrono::NaiveDate::parse_from_str(¶ms.start, fmt).unwrap();
|
||||||
|
let end_date = chrono::NaiveDate::parse_from_str(¶ms.end, fmt)
|
||||||
|
.unwrap()
|
||||||
|
.checked_sub_days(Days::new(1))
|
||||||
|
.unwrap();
|
||||||
|
let start_datetime = start_date.and_hms_opt(14, 0, 0).unwrap();
|
||||||
|
let end_datetime = end_date.and_hms_opt(10, 0, 0).unwrap();
|
||||||
|
|
||||||
|
let event = sqlx::query_scalar::<_, uuid::Uuid>(
|
||||||
|
r#"insert into calendar_events (created_by, updated_by, calendar_id, event_type_id, title, start_time, end_time)
|
||||||
|
select p.id as created_by,
|
||||||
|
p.id as updated_by,
|
||||||
|
c.id as calendar_id,
|
||||||
|
cet.id as event_type_id,
|
||||||
|
p.given_name as title,
|
||||||
|
$1 as start_time,
|
||||||
|
$2 as end_time
|
||||||
|
from calendar c,
|
||||||
|
calendar_event_types cet,
|
||||||
|
people p
|
||||||
|
where c.name = 'Cottage'
|
||||||
|
and cet.name = 'Reservation'
|
||||||
|
and cet.state = 'Requested'
|
||||||
|
and p.id = $3
|
||||||
|
returning id"#
|
||||||
|
)
|
||||||
|
.bind(start_datetime)
|
||||||
|
.bind(end_datetime)
|
||||||
|
.bind(personid)
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Error creating event: {}", e),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
//println!("Event: {:#?}", &event.unwrap());
|
||||||
|
|
||||||
|
let event_id = event.clone();
|
||||||
|
|
||||||
|
eventstring = get_event(event_id.unwrap(), &db_pool).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventstring
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_event(
|
||||||
|
State(db_pool): State<PgPool>,
|
||||||
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
Extension(rbac): Extension<RbacService>,
|
||||||
|
request: axum::http::Request<axum::body::Body>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
// Is the user logged in?
|
||||||
|
let logged_in = user_data.is_some();
|
||||||
|
|
||||||
|
// Set default events
|
||||||
|
let mut eventstring: String = "[]".to_string();
|
||||||
|
|
||||||
if logged_in {
|
if logged_in {
|
||||||
// Extract the user data.
|
// Extract the user data.
|
||||||
let _user = user_data.as_ref().unwrap().clone();
|
let _user = user_data.as_ref().unwrap().clone();
|
||||||
let _userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||||
|
let personid = user_data
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.person_id.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
if is_authorized("/calendar", user_data, db_pool.clone()).await {
|
if rbac.has_permission(userid, "calendar:*:*").await {
|
||||||
// Get requested calendar events from database
|
let (_parts, body) = request.into_parts();
|
||||||
let events = sqlx::query(r#"select to_json(json_agg(json_build_object(
|
let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||||
'title', ce.title,
|
let body_str = String::from_utf8(bytes.to_vec()).unwrap();
|
||||||
'start', ce.start_time,
|
// println!("Body: {}", body_str);
|
||||||
'end', ce.end_time,
|
|
||||||
'allDay', false)))
|
let v: Value = serde_json::from_str(&body_str).unwrap();
|
||||||
from calendar_events ce
|
|
||||||
join calendar c on c.id = ce.calendar_id
|
let fmt = "%Y-%m-%d";
|
||||||
join calendar_event_types cet on cet.id = ce.event_type_id
|
|
||||||
where ce.celebrate = true
|
let start_date = chrono::NaiveDate::parse_from_str(v["start"].as_str().unwrap(), fmt).unwrap();
|
||||||
and c.name = $1
|
let end_date = chrono::NaiveDate::parse_from_str(v["end"].as_str().unwrap(), fmt).unwrap();
|
||||||
and start_time > $2
|
let start_datetime = start_date.and_hms_opt(14, 0, 0).unwrap();
|
||||||
and start_time < $3"#)
|
let end_datetime = end_date.and_hms_opt(10, 0, 0).unwrap();
|
||||||
.bind(calendar)
|
|
||||||
.bind(chrono::DateTime::parse_from_rfc3339(¶ms.start).unwrap())
|
// Convert calendar id to UUID
|
||||||
.bind(chrono::DateTime::parse_from_rfc3339(¶ms.end).unwrap())
|
let calendar_event_id = Uuid::parse_str(v["id"].as_str().unwrap()).unwrap();
|
||||||
|
|
||||||
|
// Display values to be updated
|
||||||
|
// println!("person : {}", personid);
|
||||||
|
// println!("title : {}", v["title"]);
|
||||||
|
// println!("start : {:#?}", start_datetime);
|
||||||
|
// println!("end : {:#?}", end_datetime);
|
||||||
|
|
||||||
|
let event = sqlx::query_scalar::<_, uuid::Uuid>(
|
||||||
|
r#"with cet as (select id from calendar_event_types where name = 'Reservation' and state = 'Requested')
|
||||||
|
update calendar_events
|
||||||
|
set updated_by = $1,
|
||||||
|
updated_at = now(),
|
||||||
|
event_type_id = cet.id,
|
||||||
|
title = $2,
|
||||||
|
start_time = $3,
|
||||||
|
end_time = $4
|
||||||
|
from cet
|
||||||
|
where calendar_events.id = $5
|
||||||
|
returning calendar_events.id"#
|
||||||
|
)
|
||||||
|
.bind(personid)
|
||||||
|
.bind(v["title"].as_str().unwrap())
|
||||||
|
.bind(start_datetime)
|
||||||
|
.bind(end_datetime)
|
||||||
|
.bind(calendar_event_id)
|
||||||
.fetch_one(&db_pool)
|
.fetch_one(&db_pool)
|
||||||
.await;
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Error creating event: {}", e),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
if let Ok(json_string) = events {
|
let event_id = event.clone();
|
||||||
if let Ok(stringevents) = json_string.try_get_raw(0).map(|v| v.as_str().unwrap_or("")) {
|
|
||||||
println!("PgValue: {:?}", stringevents);
|
// println!("Event: {:#?}", event);
|
||||||
eventstring = stringevents.to_string();
|
|
||||||
}
|
eventstring = get_event(event_id.unwrap(), &db_pool).await;
|
||||||
}
|
|
||||||
|
// println!("{:#?}", eventstring);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
eventstring
|
eventstring
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn calendar_update_event_state(
|
||||||
|
State(db_pool): State<PgPool>,
|
||||||
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
Extension(rbac): Extension<RbacService>,
|
||||||
|
request: axum::http::Request<axum::body::Body>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
// Is the user logged in?
|
||||||
|
let logged_in = user_data.is_some();
|
||||||
|
|
||||||
|
// Set default events
|
||||||
|
let mut eventstring: String = "[]".to_string();
|
||||||
|
|
||||||
|
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 personid = user_data
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.person_id.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if rbac.has_permission(userid, "calendar:*:*").await {
|
||||||
|
let (_parts, body) = request.into_parts();
|
||||||
|
let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
|
||||||
|
let body_str = String::from_utf8(bytes.to_vec()).unwrap();
|
||||||
|
|
||||||
|
let v: Value = serde_json::from_str(&body_str).unwrap();
|
||||||
|
|
||||||
|
// Convert calendar id to UUID
|
||||||
|
let calendar_event_id = Uuid::parse_str(v["id"].as_str().unwrap()).unwrap();
|
||||||
|
|
||||||
|
let event = sqlx::query_scalar::<_, uuid::Uuid>(
|
||||||
|
r#"with cet as (select id from calendar_event_types where name = $1 and state = $2)
|
||||||
|
update calendar_events
|
||||||
|
set updated_by = $3,
|
||||||
|
updated_at = now(),
|
||||||
|
event_type_id = cet.id
|
||||||
|
from cet
|
||||||
|
where calendar_events.id = $4
|
||||||
|
returning calendar_events.id"#
|
||||||
|
)
|
||||||
|
.bind(v["eventType"].as_str().unwrap())
|
||||||
|
.bind(v["state"].as_str().unwrap())
|
||||||
|
.bind(personid)
|
||||||
|
.bind(calendar_event_id)
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Error creating event: {}", e),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let event_id = event.clone();
|
||||||
|
|
||||||
|
eventstring = get_event(event_id.unwrap(), &db_pool).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventstring
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "newevent.html")]
|
||||||
|
struct EventTemplate {
|
||||||
|
logged_in: bool,
|
||||||
|
user: AccountData,
|
||||||
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
|
calendars: Vec<Calendar>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_event(
|
||||||
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
State(db_pool): State<PgPool>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
// Is the user logged in?
|
||||||
|
let logged_in = user_data.is_some();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if is_authorized("/calendar", user_data, db_pool.clone()).await {
|
||||||
|
// Get user roles
|
||||||
|
let user_roles = get_user_roles_display(userid, &db_pool.clone()).await;
|
||||||
|
let calendars: Vec<Calendar> = get_calendars(db_pool.clone()).await;
|
||||||
|
|
||||||
|
let template = EventTemplate {
|
||||||
|
logged_in,
|
||||||
|
user,
|
||||||
|
user_roles,
|
||||||
|
calendars,
|
||||||
|
};
|
||||||
|
HtmlTemplate(template).into_response()
|
||||||
|
} else {
|
||||||
|
Redirect::to("/").into_response()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Redirect::to("/").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
// GOOGLE_CLIENT_SECRET=yyy
|
// GOOGLE_CLIENT_SECRET=yyy
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Extension, Host, Query, State}, response::{IntoResponse, Redirect}
|
extract::{Extension, Query, State}, response::{IntoResponse, Redirect}
|
||||||
};
|
};
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::{extract::Host, TypedHeader};
|
||||||
use dotenvy::var;
|
use dotenvy::var;
|
||||||
use headers::Cookie;
|
use headers::Cookie;
|
||||||
use http::{HeaderMap, HeaderValue};
|
use http::{HeaderMap, HeaderValue};
|
||||||
|
|
@ -21,10 +21,10 @@ use sqlx::PgPool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::user::get_useremails_by_role;
|
use crate::user::{get_useremails_by_role, AccountData};
|
||||||
use crate::email::send_emails;
|
use crate::email::send_emails;
|
||||||
|
|
||||||
use super::{AppError, UserData};
|
use super::{AppError};
|
||||||
|
|
||||||
fn get_client(hostname: String) -> Result<BasicClient, AppError> {
|
fn get_client(hostname: String) -> Result<BasicClient, AppError> {
|
||||||
let google_client_id = ClientId::new(var("GOOGLE_CLIENT_ID")?);
|
let google_client_id = ClientId::new(var("GOOGLE_CLIENT_ID")?);
|
||||||
|
|
@ -58,7 +58,7 @@ fn get_client(hostname: String) -> Result<BasicClient, AppError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
Query(mut params): Query<HashMap<String, String>>,
|
Query(mut params): Query<HashMap<String, String>>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Host(hostname): Host,
|
Host(hostname): Host,
|
||||||
|
|
@ -193,8 +193,8 @@ pub async fn google_auth_return(
|
||||||
let user_id = if let Ok(query) = query {
|
let user_id = if let Ok(query) = query {
|
||||||
query.0
|
query.0
|
||||||
} else {
|
} else {
|
||||||
// Add user
|
// Add person
|
||||||
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 person: (uuid::Uuid,) = sqlx::query_as(r#"INSERT INTO people (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(email.clone())
|
||||||
.bind(name.clone())
|
.bind(name.clone())
|
||||||
.bind(family_name.clone())
|
.bind(family_name.clone())
|
||||||
|
|
@ -202,9 +202,19 @@ pub async fn google_auth_return(
|
||||||
.fetch_one(&db_pool)
|
.fetch_one(&db_pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Add user
|
||||||
|
let user: (uuid::Uuid,) = sqlx::query_as(r#"INSERT INTO users (created_by, updated_by, email, name, family_name, given_name, person_id) VALUES ((SELECT id FROM users WHERE "name" = 'admin'), (SELECT id FROM users WHERE "name" = 'admin'), $1, $2, $3, $4, $5) RETURNING id"#)
|
||||||
|
.bind(email.clone())
|
||||||
|
.bind(name.clone())
|
||||||
|
.bind(family_name.clone())
|
||||||
|
.bind(given_name.clone())
|
||||||
|
.bind(person.0)
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Add public role
|
// 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(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(user.0)
|
||||||
.execute(&db_pool)
|
.execute(&db_pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -213,7 +223,7 @@ pub async fn google_auth_return(
|
||||||
let body = format!("A new user has registered on the website: <br> <b>Name:</b> {} <br> <b>Email:</b> {} <br> <b>Family Name:</b> {} <br> <b>Given Name:</b> {}", name, email, family_name, given_name);
|
let body = format!("A new user has registered on the website: <br> <b>Name:</b> {} <br> <b>Email:</b> {} <br> <b>Family Name:</b> {} <br> <b>Given Name:</b> {}", name, email, family_name, given_name);
|
||||||
send_emails("Jean-Marie website - New user registration".to_string(), recipients, body);
|
send_emails("Jean-Marie website - New user registration".to_string(), recipients, body);
|
||||||
|
|
||||||
query.0
|
user.0
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update session with user id or create new session
|
// Update session with user id or create new session
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
middleware,
|
middleware,
|
||||||
routing::{get, get_service},
|
routing::{get, get_service, post},
|
||||||
Extension, Router,
|
Extension, Router,
|
||||||
};
|
};
|
||||||
use dotenvy::var;
|
use dotenvy::var;
|
||||||
use secret_gift_exchange::{giftexchange, giftexchange_save, giftexchanges};
|
use secret_gift_exchange::{giftexchange, giftexchange_save, giftexchanges};
|
||||||
use sqlx::{migrate::Migrator, sqlite::SqlitePoolOptions, sqlite::SqliteRow, Row, SqlitePool};
|
use sqlx::{migrate::Migrator, sqlite::SqlitePoolOptions, Row, SqlitePool};
|
||||||
use sqlx::{postgres::PgPoolOptions, PgPool};
|
use sqlx::{postgres::PgPoolOptions, PgPool};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
|
@ -19,18 +19,23 @@ mod routes;
|
||||||
mod secret_gift_exchange;
|
mod secret_gift_exchange;
|
||||||
mod user;
|
mod user;
|
||||||
mod wishlist;
|
mod wishlist;
|
||||||
|
mod rbac;
|
||||||
|
|
||||||
use calendar::{calendar, get_events};
|
use rbac::RbacService;
|
||||||
|
use calendar::{calendar, get_events, create_event, new_event, new_request};
|
||||||
use error_handling::AppError;
|
use error_handling::AppError;
|
||||||
use google_oauth::{google_auth_return, login, logout};
|
use google_oauth::{google_auth_return, login, logout};
|
||||||
use middlewares::inject_user_data;
|
use middlewares::inject_user_data;
|
||||||
use routes::{about, contact, dashboard, index, profile, user_profile, useradmin};
|
use routes::{about, contact, dashboard, index, profile, user_profile, user_profile_account, useradmin};
|
||||||
use user::{add_user_role, delete_user_role, UserData};
|
use user::{add_user_role, delete_user_role, AccountData};
|
||||||
use wishlist::{
|
use wishlist::{
|
||||||
user_wishlist, user_wishlist_add, user_wishlist_add_item, user_wishlist_bought_item,
|
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_delete_item, user_wishlist_edit_item, user_wishlist_received_item,
|
||||||
user_wishlist_returned_item, user_wishlist_save_item, wishlists,
|
user_wishlist_returned_item, user_wishlist_save_item, wishlists,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::calendar::{calendar_update_event_state, update_event};
|
||||||
|
|
||||||
//use email::send_emails;
|
//use email::send_emails;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -69,7 +74,9 @@ async fn main() {
|
||||||
println!("SOURCE_DB_URL not set");
|
println!("SOURCE_DB_URL not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_data: Option<UserData> = None;
|
let rbac = RbacService::new(app_state.db_pool.clone());
|
||||||
|
|
||||||
|
let user_data: Option<AccountData> = None;
|
||||||
|
|
||||||
// build our application with some routes
|
// build our application with some routes
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
|
@ -77,46 +84,52 @@ async fn main() {
|
||||||
// User
|
// User
|
||||||
.route("/profile", get(profile))
|
.route("/profile", get(profile))
|
||||||
.route("/useradmin", get(useradmin))
|
.route("/useradmin", get(useradmin))
|
||||||
.route("/users/:user_id", get(user_profile))
|
.route("/user/{user_id}", get(user_profile))
|
||||||
.route("/roles/:user_id/:role_id/add", get(add_user_role))
|
.route("/user/{user_id}/{account_id}", get(user_profile_account))
|
||||||
|
.route("/roles/{user_id}/{role_id}/add", get(add_user_role))
|
||||||
.route(
|
.route(
|
||||||
"/roles/:user_id/:user_role_id/delete",
|
"/roles/{user_id}/{user_role_id}/delete",
|
||||||
get(delete_user_role),
|
get(delete_user_role),
|
||||||
)
|
)
|
||||||
// Calendar
|
// Calendar
|
||||||
.route("/calendar", get(calendar))
|
.route("/calendar", get(calendar))
|
||||||
.route("/getevents/:calendar", get(get_events))
|
.route("/calendar/getevents", get(get_events))
|
||||||
|
.route("/calendar/createevent", post(create_event))
|
||||||
|
.route("/calendar/newevent", get(new_event))
|
||||||
|
.route("/calendar/newrequest", post(new_request))
|
||||||
|
.route("/calendar/updaterequest", post(update_event))
|
||||||
|
.route("/calendar/updateeventstate", post(calendar_update_event_state))
|
||||||
// Wishlist
|
// Wishlist
|
||||||
.route("/wishlists", get(wishlists))
|
.route("/wishlists", get(wishlists))
|
||||||
.route("/userwishlist/:user_id", get(user_wishlist))
|
.route("/userwishlist/{user_id}", get(user_wishlist))
|
||||||
.route(
|
.route(
|
||||||
"/userwishlist/add/:user_id",
|
"/userwishlist/add/{user_id}",
|
||||||
get(user_wishlist_add).post(user_wishlist_add_item),
|
get(user_wishlist_add).post(user_wishlist_add_item),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/userwishlist/edit/:item_id",
|
"/userwishlist/edit/{item_id}",
|
||||||
get(user_wishlist_edit_item).post(user_wishlist_save_item),
|
get(user_wishlist_edit_item).post(user_wishlist_save_item),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/userwishlist/bought/:user_id",
|
"/userwishlist/bought/{user_id}",
|
||||||
get(user_wishlist_bought_item),
|
get(user_wishlist_bought_item),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/userwishlist/received/:user_id",
|
"/userwishlist/received/{user_id}",
|
||||||
get(user_wishlist_received_item),
|
get(user_wishlist_received_item),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/userwishlist/delete/:item_id",
|
"/userwishlist/delete/{item_id}",
|
||||||
get(user_wishlist_delete_item),
|
get(user_wishlist_delete_item),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/userwishlist/returned/:item_id",
|
"/userwishlist/returned/{item_id}",
|
||||||
get(user_wishlist_returned_item),
|
get(user_wishlist_returned_item),
|
||||||
)
|
)
|
||||||
// Secret Gift Exchange - Not ready for public use yet
|
// Secret Gift Exchange - Not ready for public use yet
|
||||||
.route("/giftexchanges", get(giftexchanges))
|
.route("/giftexchanges", get(giftexchanges))
|
||||||
.route(
|
.route(
|
||||||
"/giftexchange/:giftexchange_id",
|
"/giftexchange/{giftexchange_id}",
|
||||||
get(giftexchange).post(giftexchange_save),
|
get(giftexchange).post(giftexchange_save),
|
||||||
)
|
)
|
||||||
.nest_service(
|
.nest_service(
|
||||||
|
|
@ -135,7 +148,8 @@ async fn main() {
|
||||||
inject_user_data,
|
inject_user_data,
|
||||||
))
|
))
|
||||||
.with_state(app_state.db_pool.clone())
|
.with_state(app_state.db_pool.clone())
|
||||||
.layer(Extension(user_data));
|
.layer(Extension(user_data))
|
||||||
|
.layer(Extension(rbac));
|
||||||
|
|
||||||
// Send email indicating server has started
|
// Send email indicating server has started
|
||||||
//let recipients = get_useremails_by_role("admin".to_string(), &app_state.db_pool).await;
|
//let recipients = get_useremails_by_role("admin".to_string(), &app_state.db_pool).await;
|
||||||
|
|
@ -314,4 +328,45 @@ async fn copy_database(sdb_pool: &SqlitePool, db_pool: &PgPool) {
|
||||||
println!("Error: {}", e);
|
println!("Error: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run migration scripts again
|
||||||
|
// Copy accounts(users) to profiles(people)
|
||||||
|
let result =sqlx::query(r#"insert into people (created_by, updated_by, email, name, family_name, given_name)
|
||||||
|
select created_by, updated_by, email, name, family_name, given_name from users
|
||||||
|
on conflict do nothing;"#)
|
||||||
|
.execute(db_pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
println!("Error: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link accounts to profiles
|
||||||
|
let result = sqlx::query(r#"update users u set person_id = p.id from people p where p.email = u.email;"#)
|
||||||
|
.execute(db_pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
println!("Error: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move wishlist items from accounts to profiles
|
||||||
|
let result = sqlx::query(r#"update wishlist_items wi set user_id = p.person_id from users p where p.id = wi.user_id;"#)
|
||||||
|
.execute(db_pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
println!("Error: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy normal role from accounts to profiles
|
||||||
|
let result = sqlx::query(r#"insert into user_roles (created_at, created_by, updated_at, updated_by, user_id, role_id)
|
||||||
|
select ur.created_at, ur.created_by, ur.updated_at, ur.updated_by, u.person_id, ur.role_id from user_roles ur join roles r on r.id = ur.role_id join users u on u.id = ur.user_id where r.name = 'normal'
|
||||||
|
on conflict do nothing;"#)
|
||||||
|
.execute(db_pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
println!("Error: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use super::{AppError, UserData};
|
use super::{AppError, AccountData};
|
||||||
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 axum_extra::TypedHeader;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
@ -39,15 +39,15 @@ pub async fn inject_user_data(
|
||||||
let id = query.0;
|
let id = query.0;
|
||||||
let expires_at = query.1;
|
let expires_at = query.1;
|
||||||
if expires_at > Utc::now().naive_local() {
|
if expires_at > Utc::now().naive_local() {
|
||||||
let row: UserData = sqlx::query_as(
|
let row: AccountData = 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"#,
|
r#"SELECT id, created_at, created_by, updated_at, updated_by, email, name, family_name, given_name, person_id FROM users WHERE id = $1"#,
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_one(&db_pool)
|
.fetch_one(&db_pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
request.extensions_mut().insert(Some(UserData {
|
request.extensions_mut().insert(Some(AccountData {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
created_at: row.created_at,
|
created_at: row.created_at,
|
||||||
created_by: row.created_by,
|
created_by: row.created_by,
|
||||||
|
|
@ -57,6 +57,7 @@ pub async fn inject_user_data(
|
||||||
name: row.name,
|
name: row.name,
|
||||||
family_name: row.family_name,
|
family_name: row.family_name,
|
||||||
given_name: row.given_name,
|
given_name: row.given_name,
|
||||||
|
person_id: row.person_id,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +70,7 @@ pub async fn inject_user_data(
|
||||||
Ok(next.run(request).await)
|
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<AccountData>, db_pool: PgPool) -> bool {
|
||||||
if let Some(user_data) = user_data {
|
if let Some(user_data) = user_data {
|
||||||
let query: Result<(uuid::Uuid,), _> = match path {
|
let query: Result<(uuid::Uuid,), _> = match path {
|
||||||
"/profile" => {
|
"/profile" => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
// src/rbac.rs
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive (Clone)]
|
||||||
|
pub struct RbacService {
|
||||||
|
pool: sqlx::PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RbacService {
|
||||||
|
pub fn new(pool: sqlx::PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn has_permission(&self, user_id: Uuid, resource: &str) -> bool {
|
||||||
|
let result: Result<Vec<String>, _> = sqlx::query_scalar(
|
||||||
|
r#"
|
||||||
|
SELECT rp.item FROM roles r
|
||||||
|
INNER JOIN role_permissions rp ON r.id = rp.role_id
|
||||||
|
INNER JOIN user_roles ur ON r.id = ur.role_id
|
||||||
|
WHERE ur.user_id = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(patterns) => patterns.iter()
|
||||||
|
.any(|pattern| permission_matches(pattern, resource)),
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wildcard permission matching (e.g., "article:edit:*" matches "article:edit:123")
|
||||||
|
fn permission_matches(pattern: &str, resource: &str) -> bool {
|
||||||
|
let pattern_segments: Vec<&str> = pattern.split(':').collect();
|
||||||
|
let resource_segments: Vec<&str> = resource.split(':').collect();
|
||||||
|
|
||||||
|
if pattern_segments.len() != resource_segments.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern_segments.iter()
|
||||||
|
.zip(resource_segments.iter())
|
||||||
|
.all(|(p, r)| p == r || *p == "*" || *r == "*")
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use askama_axum::{Response, Template};
|
use askama_axum::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{Html, IntoResponse, Redirect},
|
response::{Html, IntoResponse, Redirect},
|
||||||
|
|
@ -6,18 +6,18 @@ use axum::{
|
||||||
};
|
};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
middlewares::is_authorized,
|
middlewares::is_authorized,
|
||||||
user::{get_other_roles_display, get_user_roles_display},
|
user::{get_other_roles_display, get_user_roles_display, AccountData, PersonData},
|
||||||
UserData,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "profile.html")]
|
#[template(path = "profile.html")]
|
||||||
struct ProfileTemplate {
|
struct ProfileTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
user_roles: Vec<crate::user::UserRolesDisplay>,
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,9 +25,11 @@ struct ProfileTemplate {
|
||||||
#[template(path = "user.html")]
|
#[template(path = "user.html")]
|
||||||
struct UserProfileTemplate {
|
struct UserProfileTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
user_roles: Vec<crate::user::UserRolesDisplay>,
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
profile: UserData,
|
profile: PersonData,
|
||||||
|
profile_accounts: Vec<AccountData>,
|
||||||
|
account: AccountData,
|
||||||
profile_roles: Vec<crate::user::UserRolesDisplay>,
|
profile_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
non_profile_roles: Vec<crate::user::UserRolesDisplay>,
|
non_profile_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +40,7 @@ impl<T> IntoResponse for HtmlTemplate<T>
|
||||||
where
|
where
|
||||||
T: Template,
|
T: Template,
|
||||||
{
|
{
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> http::Response<axum::body::Body> {
|
||||||
match self.0.render() {
|
match self.0.render() {
|
||||||
Ok(html) => Html(html).into_response(),
|
Ok(html) => Html(html).into_response(),
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
|
|
@ -54,21 +56,21 @@ where
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
struct IndexTemplate {
|
struct IndexTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "dashboard.html")]
|
#[template(path = "dashboard.html")]
|
||||||
struct DashboardTemplate {
|
struct DashboardTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
user_roles: Vec<crate::user::UserRolesDisplay>,
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
fire_rating: String,
|
fire_rating: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn index(
|
pub async fn index(
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
|
@ -86,7 +88,7 @@ pub async fn index(
|
||||||
} else {
|
} else {
|
||||||
let template = IndexTemplate {
|
let template = IndexTemplate {
|
||||||
logged_in,
|
logged_in,
|
||||||
user: UserData::default(),
|
user: AccountData::default(),
|
||||||
};
|
};
|
||||||
HtmlTemplate(template).into_response()
|
HtmlTemplate(template).into_response()
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +96,7 @@ pub async fn index(
|
||||||
|
|
||||||
pub async fn dashboard(
|
pub async fn dashboard(
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
|
@ -128,7 +130,7 @@ pub async fn dashboard(
|
||||||
/// Handles the profile page.
|
/// Handles the profile page.
|
||||||
pub async fn profile(
|
pub async fn profile(
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
|
@ -160,7 +162,45 @@ pub async fn profile(
|
||||||
pub async fn user_profile(
|
pub async fn user_profile(
|
||||||
Path(user_id): Path<uuid::Uuid>,
|
Path(user_id): Path<uuid::Uuid>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
// Is the user logged in?
|
||||||
|
let logged_in = user_data.is_some();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Extract the user data.
|
||||||
|
let profile: PersonData = sqlx::query_as( "SELECT * FROM people WHERE id = $1")
|
||||||
|
.bind(user_id)
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if is_authorized("/users", user_data, db_pool.clone()).await {
|
||||||
|
// Get first user account
|
||||||
|
let profile_account: Uuid = sqlx::query_scalar( "SELECT id FROM users WHERE person_id = $1 LIMIT 1")
|
||||||
|
.bind(user_id)
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let redirect_url = format!("/user/{user_id}/{}", profile_account);
|
||||||
|
Redirect::to(&redirect_url).into_response()
|
||||||
|
} else {
|
||||||
|
Redirect::to("/").into_response()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Redirect::to("/").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn user_profile_account(
|
||||||
|
Path((user_id, account_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||||
|
State(db_pool): State<PgPool>,
|
||||||
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
|
@ -171,7 +211,7 @@ pub async fn user_profile(
|
||||||
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||||
|
|
||||||
// Extract the user data.
|
// Extract the user data.
|
||||||
let profile = sqlx::query_as( "SELECT * FROM users WHERE id = $1")
|
let profile = sqlx::query_as( "SELECT * FROM people WHERE id = $1")
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_one(&db_pool)
|
.fetch_one(&db_pool)
|
||||||
.await
|
.await
|
||||||
|
|
@ -181,11 +221,36 @@ pub async fn user_profile(
|
||||||
// Get logged in user roles
|
// Get logged in user roles
|
||||||
let user_roles = get_user_roles_display(userid, &db_pool.clone()).await;
|
let user_roles = get_user_roles_display(userid, &db_pool.clone()).await;
|
||||||
|
|
||||||
|
// Get user accounts
|
||||||
|
let profile_accounts = sqlx::query_as( r#"SELECT
|
||||||
|
id,
|
||||||
|
created_at,
|
||||||
|
created_by,
|
||||||
|
updated_at,
|
||||||
|
updated_by,
|
||||||
|
person_id,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
family_name,
|
||||||
|
given_name
|
||||||
|
FROM users WHERE person_id = $1 order by name"#)
|
||||||
|
.bind(user_id)
|
||||||
|
.fetch_all(&db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Get user account
|
||||||
|
let account = sqlx::query_as( "SELECT * FROM users WHERE id = $1")
|
||||||
|
.bind(account_id)
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Get user roles
|
// Get user roles
|
||||||
let profile_roles = get_user_roles_display(user_id, &db_pool.clone()).await;
|
let profile_roles = get_user_roles_display(account_id, &db_pool.clone()).await;
|
||||||
|
|
||||||
// Get roles user does not have
|
// Get roles user does not have
|
||||||
let non_profile_roles = get_other_roles_display(user_id, &db_pool.clone()).await;
|
let non_profile_roles = get_other_roles_display(account_id, &db_pool.clone()).await;
|
||||||
|
|
||||||
// Create the profile template.
|
// Create the profile template.
|
||||||
let template = UserProfileTemplate {
|
let template = UserProfileTemplate {
|
||||||
|
|
@ -193,6 +258,8 @@ pub async fn user_profile(
|
||||||
user,
|
user,
|
||||||
user_roles,
|
user_roles,
|
||||||
profile,
|
profile,
|
||||||
|
profile_accounts,
|
||||||
|
account,
|
||||||
profile_roles,
|
profile_roles,
|
||||||
non_profile_roles,
|
non_profile_roles,
|
||||||
};
|
};
|
||||||
|
|
@ -209,13 +276,13 @@ pub async fn user_profile(
|
||||||
#[template(path = "useradmin.html")]
|
#[template(path = "useradmin.html")]
|
||||||
struct UserAdminTemplate {
|
struct UserAdminTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
user_roles: Vec<crate::user::UserRolesDisplay>,
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
users: Vec<UserData>,
|
users: Vec<PersonData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn useradmin(
|
pub async fn useradmin(
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
|
|
@ -226,7 +293,7 @@ pub async fn useradmin(
|
||||||
let user = user_data.as_ref().unwrap().clone();
|
let user = user_data.as_ref().unwrap().clone();
|
||||||
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||||
|
|
||||||
let users = sqlx::query_as::<_, UserData>("SELECT * FROM users")
|
let users = sqlx::query_as::<_, PersonData>("SELECT * FROM people order by name")
|
||||||
.fetch_all(&db_pool)
|
.fetch_all(&db_pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -254,15 +321,15 @@ pub async fn useradmin(
|
||||||
#[template(path = "about.html")]
|
#[template(path = "about.html")]
|
||||||
struct AboutTemplate {
|
struct AboutTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn about(Extension(user_data): Extension<Option<UserData>>) -> impl IntoResponse {
|
pub async fn about(Extension(user_data): Extension<Option<AccountData>>) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
|
||||||
// Set empty user
|
// Set empty user
|
||||||
let mut user = UserData::default();
|
let mut user = AccountData::default();
|
||||||
|
|
||||||
if logged_in {
|
if logged_in {
|
||||||
// Extract the user data.
|
// Extract the user data.
|
||||||
|
|
@ -277,15 +344,15 @@ pub async fn about(Extension(user_data): Extension<Option<UserData>>) -> impl In
|
||||||
#[template(path = "contactus.html")]
|
#[template(path = "contactus.html")]
|
||||||
struct ContactTemplate {
|
struct ContactTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn contact(Extension(user_data): Extension<Option<UserData>>) -> impl IntoResponse {
|
pub async fn contact(Extension(user_data): Extension<Option<AccountData>>) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
|
||||||
// Set empty user
|
// Set empty user
|
||||||
let mut user = UserData::default();
|
let mut user = AccountData::default();
|
||||||
|
|
||||||
if logged_in {
|
if logged_in {
|
||||||
// Extract the user data.
|
// Extract the user data.
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_axum::{IntoResponse, Response};
|
|
||||||
use axum::{
|
use axum::{
|
||||||
body::{self, Body},
|
body::{self, Body},
|
||||||
extract::{FromRequest, Path, Request, State},
|
extract::{Path, Request, State},
|
||||||
response::Redirect,
|
response::{Html, IntoResponse, Redirect, Response},
|
||||||
Extension, Form, Json, RequestExt,
|
Extension,
|
||||||
};
|
};
|
||||||
use axum_extra::response::Html;
|
|
||||||
use http::{header::CONTENT_TYPE, StatusCode};
|
use http::{header::CONTENT_TYPE, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{FromRow, PgPool};
|
use sqlx::{FromRow, PgPool};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
middlewares::is_authorized,
|
middlewares::is_authorized,
|
||||||
user::{get_user_roles_display, UserData},
|
user::{get_user_roles_display, AccountData, PersonData},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Select participants from user list
|
/// Select participants from user list
|
||||||
|
|
@ -89,13 +87,13 @@ struct GiftExchange {
|
||||||
#[template(path = "giftexchanges.html")]
|
#[template(path = "giftexchanges.html")]
|
||||||
struct GiftExchangesTemplate {
|
struct GiftExchangesTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
user_roles: Vec<crate::user::UserRolesDisplay>,
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
giftexchanges: Vec<GiftExchange>,
|
giftexchanges: Vec<GiftExchange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn giftexchanges(
|
pub async fn giftexchanges(
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
|
|
@ -146,16 +144,16 @@ struct GiftExchangeParticipant {
|
||||||
#[template(path = "giftexchange.html")]
|
#[template(path = "giftexchange.html")]
|
||||||
struct GiftExchangeTemplate {
|
struct GiftExchangeTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
user_roles: Vec<crate::user::UserRolesDisplay>,
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
giftexchange: GiftExchange,
|
giftexchange: GiftExchange,
|
||||||
participants: Vec<UserData>,
|
participants: Vec<PersonData>,
|
||||||
non_participants: Vec<UserData>,
|
non_participants: Vec<PersonData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn giftexchange(
|
pub async fn giftexchange(
|
||||||
Path(exchange_id): Path<i64>,
|
Path(exchange_id): Path<i64>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
|
|
@ -182,7 +180,7 @@ pub async fn giftexchange(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get participants
|
// Get participants
|
||||||
let participants = sqlx::query_as::<_, UserData>(
|
let participants = sqlx::query_as::<_, PersonData>(
|
||||||
"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 = $1)",
|
||||||
)
|
)
|
||||||
.bind(exchange_id)
|
.bind(exchange_id)
|
||||||
|
|
@ -191,7 +189,7 @@ pub async fn giftexchange(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Get non participants
|
// Get non participants
|
||||||
let non_participants = sqlx::query_as::<_, UserData>(
|
let non_participants = sqlx::query_as::<_, PersonData>(
|
||||||
"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 = $1)",
|
||||||
)
|
)
|
||||||
.bind(exchange_id)
|
.bind(exchange_id)
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
use askama_axum::IntoResponse;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::Redirect,
|
response::{IntoResponse, Redirect},
|
||||||
Extension,
|
Extension,
|
||||||
};
|
};
|
||||||
|
|
||||||
///User related structs and functions
|
///User related structs and functions
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{FromRow, PgPool};
|
use sqlx::{FromRow, PgPool};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::middlewares::is_authorized;
|
use crate::middlewares::is_authorized;
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)]
|
#[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)]
|
||||||
pub struct UserData {
|
pub struct PersonData {
|
||||||
pub id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
pub created_at: chrono::NaiveDateTime,
|
pub created_at: chrono::NaiveDateTime,
|
||||||
pub created_by: uuid::Uuid,
|
pub created_by: uuid::Uuid,
|
||||||
|
|
@ -24,6 +24,20 @@ pub struct UserData {
|
||||||
pub given_name: String,
|
pub given_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug, Serialize, Deserialize, FromRow)]
|
||||||
|
pub struct AccountData {
|
||||||
|
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 person_id: uuid::Uuid,
|
||||||
|
pub email: String,
|
||||||
|
pub name: String,
|
||||||
|
pub family_name: String,
|
||||||
|
pub given_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct RoleData {
|
pub struct RoleData {
|
||||||
pub id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
|
|
@ -87,10 +101,13 @@ pub async fn get_user_roles(user_id: i64, db_pool: &PgPool) -> Vec<UserRoles> {
|
||||||
user_roles
|
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: uuid::Uuid,
|
||||||
|
db_pool: &PgPool,
|
||||||
|
) -> Vec<UserRolesDisplay> {
|
||||||
// Get user roles
|
// Get user roles
|
||||||
let user_roles = sqlx::query_as(
|
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 = $1 order by r.name"#
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_all(db_pool)
|
.fetch_all(db_pool)
|
||||||
|
|
@ -100,10 +117,13 @@ pub async fn get_user_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> Ve
|
||||||
user_roles
|
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: uuid::Uuid,
|
||||||
|
db_pool: &PgPool,
|
||||||
|
) -> Vec<UserRolesDisplay> {
|
||||||
// Get roles user does not have
|
// Get roles user does not have
|
||||||
let user_roles = sqlx::query_as(
|
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 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) order by r.name"#
|
||||||
)
|
)
|
||||||
.bind(user_id.clone())
|
.bind(user_id.clone())
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
|
|
@ -115,23 +135,28 @@ pub async fn get_other_roles_display(user_id: uuid::Uuid, db_pool: &PgPool) -> V
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_user_role(
|
pub async fn add_user_role(
|
||||||
Path((user_id, role_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
Path((account_id, role_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if is_authorized("/roles", user_data.clone(), db_pool.clone()).await {
|
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)")
|
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(user_data.as_ref().unwrap().id)// Created by current user
|
||||||
.bind(user_data.as_ref().unwrap().id) // Updated by current user
|
.bind(user_data.as_ref().unwrap().id) // Updated by current user
|
||||||
.bind(user_id)
|
.bind(account_id)
|
||||||
.bind(role_id)
|
.bind(role_id)
|
||||||
.execute(&db_pool)
|
.execute(&db_pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// TODO - send email to user regarding role changes
|
// TODO - send email to user regarding role changes
|
||||||
|
let profile_id: Uuid = sqlx::query_scalar("SELECT person_id FROM users WHERE id = $1")
|
||||||
|
.bind(account_id)
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let redirect_url = format!("/users/{user_id}");
|
let redirect_url = format!("/user/{profile_id}/{account_id}");
|
||||||
Redirect::to(&redirect_url).into_response()
|
Redirect::to(&redirect_url).into_response()
|
||||||
} else {
|
} else {
|
||||||
Redirect::to("/").into_response()
|
Redirect::to("/").into_response()
|
||||||
|
|
@ -139,9 +164,9 @@ pub async fn add_user_role(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_user_role(
|
pub async fn delete_user_role(
|
||||||
Path((user_id, user_role_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
Path((account_id, user_role_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if is_authorized("/roles", user_data, db_pool.clone()).await {
|
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 = $1")
|
||||||
|
|
@ -150,14 +175,23 @@ pub async fn delete_user_role(
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let redirect_url = format!("/users/{user_id}");
|
let profile_id: Uuid = sqlx::query_scalar("SELECT person_id FROM users WHERE id = $1")
|
||||||
|
.bind(account_id)
|
||||||
|
.fetch_one(&db_pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let redirect_url = format!("/user/{profile_id}/{account_id}");
|
||||||
Redirect::to(&redirect_url).into_response()
|
Redirect::to(&redirect_url).into_response()
|
||||||
} else {
|
} else {
|
||||||
Redirect::to("/").into_response()
|
Redirect::to("/").into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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: uuid::Uuid,
|
||||||
|
db_pool: &PgPool,
|
||||||
|
) -> UserWishlistItem {
|
||||||
// Get wish list items for the user
|
// Get wish list items for the user
|
||||||
let user_wishlist_item = sqlx::query_as(
|
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
|
r#"select id, created_at, created_by, updated_at, updated_by, user_id, item, item_url, purchased_by, received_at
|
||||||
|
|
@ -171,7 +205,10 @@ pub async fn get_user_wishlist_item_by_id(item_id: uuid::Uuid, db_pool: &PgPool)
|
||||||
user_wishlist_item
|
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: uuid::Uuid,
|
||||||
|
db_pool: &PgPool,
|
||||||
|
) -> Vec<UserWishlistItem> {
|
||||||
// Get wish list items for the user
|
// Get wish list items for the user
|
||||||
let user_wishlist_items = sqlx::query_as(
|
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
|
r#"select id, created_at, created_by, updated_at, updated_by, user_id, item, item_url, purchased_by, received_at
|
||||||
|
|
@ -194,3 +231,23 @@ pub async fn get_useremails_by_role(role_name: String, db_pool: &PgPool) -> Stri
|
||||||
|
|
||||||
useremails
|
useremails
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn user_account(
|
||||||
|
Path(account_id): Path<uuid::Uuid>,
|
||||||
|
State(db_pool): State<PgPool>,
|
||||||
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
// Is the user logged in?
|
||||||
|
let logged_in = user_data.is_some();
|
||||||
|
|
||||||
|
if logged_in {
|
||||||
|
// Extract the user data.
|
||||||
|
if is_authorized("/users", user_data.clone(), db_pool.clone()).await {
|
||||||
|
Redirect::to("/").into_response()
|
||||||
|
} else {
|
||||||
|
Redirect::to("/").into_response()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Redirect::to("/").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
use askama_axum::{IntoResponse, Response, Template};
|
use askama_axum::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::Redirect,
|
response::{Html, IntoResponse, Redirect},
|
||||||
Extension, Form,
|
Extension, Form,
|
||||||
};
|
};
|
||||||
use axum_extra::response::Html;
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -14,7 +13,7 @@ use uuid::Uuid;
|
||||||
use crate::{
|
use crate::{
|
||||||
middlewares::is_authorized,
|
middlewares::is_authorized,
|
||||||
user::{
|
user::{
|
||||||
get_user_roles_display, get_user_wishlist_item_by_id, get_user_wishlist_items, UserData,
|
get_user_roles_display, get_user_wishlist_item_by_id, get_user_wishlist_items, PersonData, AccountData,
|
||||||
UserWishlistItem,
|
UserWishlistItem,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -25,7 +24,7 @@ impl<T> IntoResponse for HtmlTemplate<T>
|
||||||
where
|
where
|
||||||
T: Template,
|
T: Template,
|
||||||
{
|
{
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> http::Response<axum::body::Body> {
|
||||||
match self.0.render() {
|
match self.0.render() {
|
||||||
Ok(html) => Html(html).into_response(),
|
Ok(html) => Html(html).into_response(),
|
||||||
Err(err) => (
|
Err(err) => (
|
||||||
|
|
@ -41,13 +40,13 @@ where
|
||||||
#[template(path = "userwishlists.html")]
|
#[template(path = "userwishlists.html")]
|
||||||
struct WishListsTemplate {
|
struct WishListsTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
user_roles: Vec<crate::user::UserRolesDisplay>,
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
users: Vec<UserData>,
|
users: Vec<PersonData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn wishlists(
|
pub async fn wishlists(
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
|
|
@ -58,7 +57,10 @@ pub async fn wishlists(
|
||||||
let user = user_data.as_ref().unwrap().clone();
|
let user = user_data.as_ref().unwrap().clone();
|
||||||
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||||
|
|
||||||
let users = sqlx::query_as::<_, UserData>("SELECT * FROM users")
|
let users = sqlx::query_as::<_, PersonData>(r#"SELECT p.* FROM people p
|
||||||
|
left join user_roles ur on ur.user_id = p.id
|
||||||
|
left join roles r on r.id = ur.role_id
|
||||||
|
where r.name = 'normal' order by p.name;"#)
|
||||||
.fetch_all(&db_pool)
|
.fetch_all(&db_pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -86,17 +88,17 @@ pub async fn wishlists(
|
||||||
#[template(path = "userwishlist.html")]
|
#[template(path = "userwishlist.html")]
|
||||||
struct UserWishListTemplate {
|
struct UserWishListTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
user_roles: Vec<crate::user::UserRolesDisplay>,
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
my_wishlist: bool,
|
my_wishlist: bool,
|
||||||
person: UserData,
|
person: PersonData,
|
||||||
person_wishlist_items: Vec<crate::user::UserWishlistItem>,
|
person_wishlist_items: Vec<crate::user::UserWishlistItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user_wishlist(
|
pub async fn user_wishlist(
|
||||||
Path(user_id): Path<uuid::Uuid>,
|
Path(user_id): Path<uuid::Uuid>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
|
@ -107,7 +109,7 @@ pub async fn user_wishlist(
|
||||||
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||||
|
|
||||||
// Extract the user data.
|
// Extract the user data.
|
||||||
let person = sqlx::query_as("SELECT * FROM users WHERE id = $1")
|
let person = sqlx::query_as("SELECT * FROM people WHERE id = $1 or id = (select person_id from users where id = $1)")
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_one(&db_pool)
|
.fetch_one(&db_pool)
|
||||||
.await
|
.await
|
||||||
|
|
@ -121,7 +123,7 @@ pub async fn user_wishlist(
|
||||||
let person_wishlist_items = get_user_wishlist_items(user_id, &db_pool.clone()).await;
|
let person_wishlist_items = get_user_wishlist_items(user_id, &db_pool.clone()).await;
|
||||||
|
|
||||||
// Is viewed and viewing user the same (my wishlist)?
|
// Is viewed and viewing user the same (my wishlist)?
|
||||||
let my_wishlist = user_id == userid;
|
let my_wishlist = user_id == user.person_id;
|
||||||
|
|
||||||
// Create the wishlist template.
|
// Create the wishlist template.
|
||||||
let template = UserWishListTemplate {
|
let template = UserWishListTemplate {
|
||||||
|
|
@ -145,16 +147,16 @@ pub async fn user_wishlist(
|
||||||
#[template(path = "userwishlistadd.html")]
|
#[template(path = "userwishlistadd.html")]
|
||||||
struct UserWishListAddTemplate {
|
struct UserWishListAddTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
user_roles: Vec<crate::user::UserRolesDisplay>,
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
person: UserData,
|
person: PersonData,
|
||||||
person_wishlist_items: Vec<crate::user::UserWishlistItem>,
|
person_wishlist_items: Vec<crate::user::UserWishlistItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user_wishlist_add(
|
pub async fn user_wishlist_add(
|
||||||
Path(user_id): Path<uuid::Uuid>,
|
Path(user_id): Path<uuid::Uuid>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
|
@ -165,7 +167,7 @@ pub async fn user_wishlist_add(
|
||||||
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
let userid = user_data.as_ref().map(|s| s.id.clone()).unwrap_or_default();
|
||||||
|
|
||||||
// Extract the user data.
|
// Extract the user data.
|
||||||
let person = sqlx::query_as("SELECT * FROM users WHERE id = $1")
|
let person = sqlx::query_as("SELECT * FROM people WHERE id = $1")
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_one(&db_pool)
|
.fetch_one(&db_pool)
|
||||||
.await
|
.await
|
||||||
|
|
@ -204,7 +206,7 @@ pub struct ItemForm {
|
||||||
pub async fn user_wishlist_add_item(
|
pub async fn user_wishlist_add_item(
|
||||||
Path(user_id): Path<uuid::Uuid>,
|
Path(user_id): Path<uuid::Uuid>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
Form(item_form): Form<ItemForm>,
|
Form(item_form): Form<ItemForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
|
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
|
||||||
|
|
@ -219,7 +221,9 @@ pub async fn user_wishlist_add_item(
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let redirect_string = format!("/userwishlist/{user_id}");
|
// Redirect to wishlist
|
||||||
|
let person_id = user_data.as_ref().unwrap().person_id;
|
||||||
|
let redirect_string = format!("/userwishlist/{person_id}");
|
||||||
Redirect::to(&redirect_string).into_response()
|
Redirect::to(&redirect_string).into_response()
|
||||||
} else {
|
} else {
|
||||||
Redirect::to("/").into_response()
|
Redirect::to("/").into_response()
|
||||||
|
|
@ -230,7 +234,7 @@ pub async fn user_wishlist_add_item(
|
||||||
#[template(path = "userwishlistedit.html")]
|
#[template(path = "userwishlistedit.html")]
|
||||||
struct UserWishListEditTemplate {
|
struct UserWishListEditTemplate {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
user: UserData,
|
user: AccountData,
|
||||||
user_roles: Vec<crate::user::UserRolesDisplay>,
|
user_roles: Vec<crate::user::UserRolesDisplay>,
|
||||||
user_wishlist_item: crate::user::UserWishlistItem,
|
user_wishlist_item: crate::user::UserWishlistItem,
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +242,7 @@ struct UserWishListEditTemplate {
|
||||||
pub async fn user_wishlist_edit_item(
|
pub async fn user_wishlist_edit_item(
|
||||||
Path(item_id): Path<uuid::Uuid>,
|
Path(item_id): Path<uuid::Uuid>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Is the user logged in?
|
// Is the user logged in?
|
||||||
let logged_in = user_data.is_some();
|
let logged_in = user_data.is_some();
|
||||||
|
|
@ -275,7 +279,7 @@ pub async fn user_wishlist_edit_item(
|
||||||
pub async fn user_wishlist_save_item(
|
pub async fn user_wishlist_save_item(
|
||||||
Path(item_id): Path<uuid::Uuid>,
|
Path(item_id): Path<uuid::Uuid>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
Form(item_form): Form<ItemForm>,
|
Form(item_form): Form<ItemForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
|
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
|
||||||
|
|
@ -292,8 +296,8 @@ pub async fn user_wishlist_save_item(
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let user_id = user_data.as_ref().unwrap().id;
|
let person_id = user_data.as_ref().unwrap().person_id;
|
||||||
let redirect_string = format!("/userwishlist/{user_id}");
|
let redirect_string = format!("/userwishlist/{person_id}");
|
||||||
Redirect::to(&redirect_string).into_response()
|
Redirect::to(&redirect_string).into_response()
|
||||||
} else {
|
} else {
|
||||||
Redirect::to("/").into_response()
|
Redirect::to("/").into_response()
|
||||||
|
|
@ -303,7 +307,7 @@ pub async fn user_wishlist_save_item(
|
||||||
pub async fn user_wishlist_bought_item(
|
pub async fn user_wishlist_bought_item(
|
||||||
Path(item_id): Path<uuid::Uuid>,
|
Path(item_id): Path<uuid::Uuid>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
|
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
|
||||||
// Update item to purchased
|
// Update item to purchased
|
||||||
|
|
@ -333,7 +337,7 @@ pub async fn user_wishlist_bought_item(
|
||||||
pub async fn user_wishlist_received_item(
|
pub async fn user_wishlist_received_item(
|
||||||
Path(user_id): Path<uuid::Uuid>,
|
Path(user_id): Path<uuid::Uuid>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
|
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
|
||||||
// Update item received time
|
// Update item received time
|
||||||
|
|
@ -347,8 +351,8 @@ pub async fn user_wishlist_received_item(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Redirect to user wishlist
|
// Redirect to user wishlist
|
||||||
let userid = user_data.as_ref().unwrap().id;
|
let person_id = user_data.as_ref().unwrap().person_id;
|
||||||
let redirect_string = format!("/userwishlist/{userid}");
|
let redirect_string = format!("/userwishlist/{person_id}");
|
||||||
Redirect::to(&redirect_string).into_response()
|
Redirect::to(&redirect_string).into_response()
|
||||||
} else {
|
} else {
|
||||||
Redirect::to("/").into_response()
|
Redirect::to("/").into_response()
|
||||||
|
|
@ -358,7 +362,7 @@ pub async fn user_wishlist_received_item(
|
||||||
pub async fn user_wishlist_delete_item(
|
pub async fn user_wishlist_delete_item(
|
||||||
Path(item_id): Path<uuid::Uuid>,
|
Path(item_id): Path<uuid::Uuid>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
|
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 = $1")
|
||||||
|
|
@ -368,8 +372,8 @@ pub async fn user_wishlist_delete_item(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Redirect to user wishlist
|
// Redirect to user wishlist
|
||||||
let userid = user_data.as_ref().unwrap().id;
|
let person_id = user_data.as_ref().unwrap().person_id;
|
||||||
let redirect_string = format!("/userwishlist/{userid}");
|
let redirect_string = format!("/userwishlist/{person_id}");
|
||||||
Redirect::to(&redirect_string).into_response()
|
Redirect::to(&redirect_string).into_response()
|
||||||
} else {
|
} else {
|
||||||
Redirect::to("/").into_response()
|
Redirect::to("/").into_response()
|
||||||
|
|
@ -379,7 +383,7 @@ pub async fn user_wishlist_delete_item(
|
||||||
pub async fn user_wishlist_returned_item(
|
pub async fn user_wishlist_returned_item(
|
||||||
Path(item_id): Path<uuid::Uuid>,
|
Path(item_id): Path<uuid::Uuid>,
|
||||||
State(db_pool): State<PgPool>,
|
State(db_pool): State<PgPool>,
|
||||||
Extension(user_data): Extension<Option<UserData>>,
|
Extension(user_data): Extension<Option<AccountData>>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if is_authorized("/wishlist", user_data.clone(), db_pool.clone()).await {
|
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 = null where id = $1")
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/useradmin">User Administration</a></li>
|
<li><a href="/useradmin">User Administration</a></li>
|
||||||
<li><a href="/giftexchanges">Gift Exchanges</a></li>
|
<li><a href="/giftexchanges">Gift Exchanges</a></li>
|
||||||
|
<li><a href="/newevent">Add Event</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: "Montserrat", sans-serif;
|
font-family: "Montserrat", sans-serif;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% block links %}{% endblock links %}
|
{% block links %}{% endblock links %}
|
||||||
|
|
@ -30,7 +32,8 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container-fluid" height="100%">
|
<div class="container-fluid" height="100vh">
|
||||||
|
<div class="row vh-100">
|
||||||
|
|
||||||
<!-- HEADER -->
|
<!-- HEADER -->
|
||||||
<div class="row fixed-top sticky-top">
|
<div class="row fixed-top sticky-top">
|
||||||
|
|
@ -64,7 +67,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<div class="row flex-grow-1">
|
<div class="row flex-1">
|
||||||
{% block content %}{% endblock content %}
|
{% block content %}{% endblock content %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -72,10 +75,11 @@
|
||||||
<div class="row fixed-bottom sticky-bottom">
|
<div class="row fixed-bottom sticky-bottom">
|
||||||
<div class="container-fluid text-center bg-light">
|
<div class="container-fluid text-center bg-light">
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2024 Jean-Marie family</p>
|
<p>© 2025 Jean-Marie family</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div><!-- /.container -->
|
</div><!-- /.container -->
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,34 +4,90 @@
|
||||||
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/@fullcalendar/daygrid@4.3.0/main.min.css'>
|
<link rel='stylesheet' href='https://cdn.jsdelivr.net/npm/@fullcalendar/daygrid@4.3.0/main.min.css'>
|
||||||
{% endblock links %}
|
{% endblock links %}
|
||||||
{% block center %}
|
{% block center %}
|
||||||
<h1>Calendar</h1>
|
|
||||||
<div class="mh-100">
|
<div class="mh-100">
|
||||||
<div id="calendar" class="fc"></div>
|
<div id="calendar" class="fc"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Create Event Modal -->
|
||||||
<div class="modal fade" id="eventDetailsModal" tabindex="-1" role="dialog" aria-labelledby="eventDetailsModalTitle"
|
<div class="modal fade" id="eventDetailsModal" tabindex="-1" role="dialog" aria-labelledby="eventDetailsModalTitle"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<form id="eventDetailsModalForm">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="eventDetailsModalTitle">Event details</h5>
|
<h5 class="modal-title" id="eventDetailsModalTitle">Request dates</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p id="eventDetailsModalDateRange"></p>
|
<div class="form-group">
|
||||||
<p id="eventDetailsModalBody">Cottage request</p>
|
<label for="eventTitle">Reservation for</label>
|
||||||
|
<input type="text" id="eventTitle" class="form-control" placeholder="Reservation for">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="eventStart">Arriving</label>
|
||||||
|
<input type="date" class="form-control" id="eventStart" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="eventEnd">Leaving</label>
|
||||||
|
<input type="date" class="form-control" id="eventEnd" required>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
<button type="button" id="eventDetailsModalClose" class="btn btn-secondary"
|
||||||
<button type="button" class="btn btn-primary">Save changes</button>
|
data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Send Request</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Event Modal -->
|
||||||
|
<div class="modal fade" id="eventEditModal" tabindex="-1" role="dialog" aria-labelledby="eventEditModalTitle"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<form id="eventForm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Edit Event</h5>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="eventEditTitle">Reservation for</label>
|
||||||
|
<input type="text" id="eventEditTitle" class="form-control" placeholder="Event Title">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="eventEditEnd">Arriving</label>
|
||||||
|
<input type="date" id="eventEditStart" class="form-control" placeholder="Start">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="eventEditEnd">Leaving</label>
|
||||||
|
<input type="date" id="eventEditEnd" class="form-control" placeholder="End">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for user_role in user_roles %}
|
||||||
|
{% if user_role.role_name == "admin" %}
|
||||||
|
<h5>Actions</h5>
|
||||||
|
<ul>
|
||||||
|
<button type="button" id="eventApprove" onclick="updateEventState('Approved')">Approve</button>
|
||||||
|
<button type="button" id="eventReject" onclick="updateEventState('Rejected')">Reject</button>
|
||||||
|
<button type="button" id="eventConfirm" onclick="updateEventState('Confirmed')">Confirm</button>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" id="closeEventEdit"
|
||||||
|
data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="saveEvent">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock center %}
|
{% endblock center %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js'></script>
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.17/index.global.min.js'></script>
|
||||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js'></script>
|
<script src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js'></script>
|
||||||
<script src='https://cdn.jsdelivr.net/npm/uuid@8.3.2/dist/umd/uuidv4.min.js'></script>
|
<script src='https://cdn.jsdelivr.net/npm/uuid@8.3.2/dist/umd/uuidv4.min.js'></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -50,29 +106,146 @@
|
||||||
center: 'title',
|
center: 'title',
|
||||||
right: 'dayGridMonth,timeGridWeek,timeGridDay,multiMonthYear'
|
right: 'dayGridMonth,timeGridWeek,timeGridDay,multiMonthYear'
|
||||||
},
|
},
|
||||||
eventSources: [
|
events: '/calendar/getevents',
|
||||||
{% for calendar in calendars %}
|
|
||||||
{
|
|
||||||
url: '/getevents/{{calendar.name}}',
|
|
||||||
color: '{{calendar.colour}}',
|
|
||||||
},
|
|
||||||
{% endfor %}
|
|
||||||
],
|
|
||||||
select: function (info) {
|
select: function (info) {
|
||||||
|
$('#eventStart').val(info.startStr);
|
||||||
|
$('#eventEnd').val(info.endStr);
|
||||||
$('#eventDetailsModal').modal('show');
|
$('#eventDetailsModal').modal('show');
|
||||||
$('#eventDetailsModalDateRange').text(info.startStr + ' to ' + info.endStr);
|
},
|
||||||
|
eventClick: function (info) {
|
||||||
info.view.calendar.addEvent({
|
if (info.event.title != "In use") {
|
||||||
id: uuidv4(),
|
$('#eventEditTitle').val(info.event.title);
|
||||||
title: 'Cottage request',
|
$('#eventEditStart').val(moment(info.event.start).format('YYYY-MM-DD'));
|
||||||
start: info.startStr + 'T14:00:00',
|
$('#eventEditEnd').val(moment(info.event.end).format('YYYY-MM-DD'));
|
||||||
end: info.endStr + 'T10:00:00',
|
$('#eventEditModal').modal('show');
|
||||||
allDay: false
|
// Store the event for later update
|
||||||
});
|
window.calEvent = info.event;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
calendar.render();
|
calendar.render();
|
||||||
|
|
||||||
|
document.getElementById('eventDetailsModalForm').addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var start = document.getElementById('eventStart').value;
|
||||||
|
var end = document.getElementById('eventEnd').value;
|
||||||
|
|
||||||
|
// Prepare the event data
|
||||||
|
var eventData = {
|
||||||
|
start: start,
|
||||||
|
end: end
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send data to the API
|
||||||
|
fetch('/calendar/newrequest', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(eventData)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('Network response was not ok');
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Optionally, use the response to add the event to the calendar
|
||||||
|
$('#eventDetailsModal').modal('hide');
|
||||||
|
calendar.addEvent({
|
||||||
|
title: data.title,
|
||||||
|
start: data.start,
|
||||||
|
end: data.end,
|
||||||
|
allDay: data.allDay,
|
||||||
|
backgroundColor: data.backgroundColor
|
||||||
});
|
});
|
||||||
|
e.target.reset();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error creating event:', error);
|
||||||
|
alert('An error occurred while creating the event.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('eventDetailsModalClose').addEventListener('click', function () {
|
||||||
|
$('#eventDetailsModal').modal('hide');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#saveEvent').on('click', function () {
|
||||||
|
var updatedEvent = {
|
||||||
|
id: window.calEvent.id,
|
||||||
|
title: $('#eventEditTitle').val(),
|
||||||
|
start: $('#eventEditStart').val(),
|
||||||
|
end: $('#eventEditEnd').val()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the updates to the record
|
||||||
|
fetch('/calendar/updaterequest', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(updatedEvent)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('Network response was not ok');
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Optionally, use the response to add the event to the calendar
|
||||||
|
$('#eventDetailsModal').modal('hide');
|
||||||
|
// Update the original event object
|
||||||
|
if (window.calEvent.title != data.title) {
|
||||||
|
window.calEvent.setProp('title', data.title);
|
||||||
|
}
|
||||||
|
if (window.calEvent.start != data.start) {
|
||||||
|
window.calEvent.setStart(data.start);
|
||||||
|
}
|
||||||
|
if (window.calEvent.end != data.end) {
|
||||||
|
window.calEvent.setEnd(data.end);
|
||||||
|
}
|
||||||
|
window.calEvent.setProp('allDay', data.allDay);
|
||||||
|
window.calEvent.setProp('backgroundColor', data.backgroundColor);
|
||||||
|
|
||||||
|
e.target.reset();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error creating event:', error);
|
||||||
|
//alert('An error occurred while creating the event.');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide the popup
|
||||||
|
$('#eventEditModal').modal('hide');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#closeEventEdit').on('click', function () {
|
||||||
|
$('#eventEditModal').modal('hide');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateEventState(state) {
|
||||||
|
var updatedEvent = {
|
||||||
|
id: window.calEvent.id,
|
||||||
|
eventType: 'Reservation',
|
||||||
|
state: state
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the updates to the record
|
||||||
|
fetch('/calendar/updateeventstate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(updatedEvent)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('Network response was not ok');
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// Update the original event object
|
||||||
|
window.calEvent.setProp('backgroundColor', data.backgroundColor);
|
||||||
|
|
||||||
|
e.target.reset();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error creating event:', error);
|
||||||
|
//alert('An error occurred while creating the event.');
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
{% extends "authorized.html" %}
|
||||||
|
{% block center %}
|
||||||
|
<h1>Create Event</h1>
|
||||||
|
|
||||||
|
<form method="post" action="/createevent">
|
||||||
|
<div>
|
||||||
|
<label for="title">Title:</label>
|
||||||
|
<input type="text" id="title" name="title" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="description">Description:</label>
|
||||||
|
<textarea id="description" name="description"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="state">State:</label>
|
||||||
|
<select id="state" name="state" required>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="approved">Approved</option>
|
||||||
|
<option value="rejected">Rejected</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="calendar_id">Calendar ID:</label>
|
||||||
|
<select id="calendar_id" name="calendar_id" required>
|
||||||
|
{% for cal in calendars %}
|
||||||
|
<option value="{{ cal.id }}">{{ cal.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="event_type_id">Event Type ID:</label>
|
||||||
|
<input type="text" id="event_type_id" name="event_type_id" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="start_time">Start Date:</label>
|
||||||
|
<input type="date" id="start_time" name="start_time" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="end_time">End Date:</label>
|
||||||
|
<input type="date" id="end_time" name="end_time" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Create Event</button>
|
||||||
|
</form>
|
||||||
|
{% endblock center %}
|
||||||
|
|
@ -5,9 +5,29 @@
|
||||||
Full name: {{ profile.name }}<br/>
|
Full name: {{ profile.name }}<br/>
|
||||||
Given name: {{ profile.given_name }}<br/>
|
Given name: {{ profile.given_name }}<br/>
|
||||||
Family name: {{ profile.family_name }}<br/>
|
Family name: {{ profile.family_name }}<br/>
|
||||||
Your email address: {{ profile.email }}<br/>
|
Logged in email address: {{ profile.email }}<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<h2>User Roles</h2>
|
<h2>Accounts</h2>
|
||||||
|
<button type="button" class="btn btn-primary">Merge</button>
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Email</th>
|
||||||
|
<th scope="col">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user_account in profile_accounts %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/user/{{profile.id}}/{{ user_account.id }}">{{ account.name }}</a></td>
|
||||||
|
<td>{{ user_account.email }}</td>
|
||||||
|
<td><a href="/account/{{ user_account.id }}/delete">Delete</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2>Account Roles ({{ account.email }})</h2>
|
||||||
<button type="button" class="btn btn-primary">Edit</button>
|
<button type="button" class="btn btn-primary">Edit</button>
|
||||||
<button type="button" class="btn btn-primary">Add</button>
|
<button type="button" class="btn btn-primary">Add</button>
|
||||||
<table class="table table-striped table-bordered">
|
<table class="table table-striped table-bordered">
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/users/{{ user.id }}">{{ user.name }}</a></td>
|
<td><a href="/user/{{ user.id }}">{{ user.name }}</a></td>
|
||||||
<td>{{ user.email }}</td>
|
<td>{{ user.email }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<br />
|
<br />
|
||||||
<h2>List</h2>
|
<h2>List</h2>
|
||||||
{% if my_wishlist %}
|
{% if my_wishlist %}
|
||||||
<a href="/userwishlist/add/{{ user.id }}">Add</a>
|
<a href="/userwishlist/add/{{ person.id }}">Add</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="table-responsive overflow-auto">
|
<div class="table-responsive overflow-auto">
|
||||||
<table data-toggle="table" class="table table-striped table-bordered">
|
<table data-toggle="table" class="table table-striped table-bordered">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
cargo build --release
|
cargo build --release
|
||||||
ssh www@192.168.59.11 'pkill jean-marie'
|
|
||||||
scp target/release/jean-marie www@192.168.59.11:/opt/jean-marie
|
scp target/release/jean-marie www@192.168.59.11:/opt/jean-marie
|
||||||
scp runsite.sh www@192.168.59.11:/opt/jean-marie
|
#scp prod.env www@192.168.59.11:/opt/jean-marie
|
||||||
scp .env www@192.168.59.11:/opt/jean-marie
|
|
||||||
scp -r templates www@192.168.59.11:/opt/jean-marie
|
scp -r templates www@192.168.59.11:/opt/jean-marie
|
||||||
ssh www@192.168.59.11 '/opt/jean-marie/runsite.sh'
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@ cargo build --release
|
||||||
ssh chris@192.168.59.31 'pkill jean-marie'
|
ssh chris@192.168.59.31 'pkill jean-marie'
|
||||||
scp target/release/jean-marie chris@192.168.59.31:/opt/jean-marie
|
scp target/release/jean-marie chris@192.168.59.31:/opt/jean-marie
|
||||||
scp runsite.sh chris@192.168.59.31:/opt/jean-marie
|
scp runsite.sh chris@192.168.59.31:/opt/jean-marie
|
||||||
#scp .env chris@192.168.59.31:/opt/jean-marie
|
scp test.env chris@192.168.59.31:/opt/jean-marie/.env
|
||||||
scp -r templates chris@192.168.59.31:/opt/jean-marie
|
scp -r templates chris@192.168.59.31:/opt/jean-marie
|
||||||
ssh chris@192.168.59.31 '/opt/jean-marie/runsite.sh'
|
ssh chris@192.168.59.31 '/opt/jean-marie/runsite.sh'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
proxy=99.232.244.189
|
||||||
|
cargo build --release
|
||||||
|
scp target/release/jean-marie chris@$proxy:development/jean-marie/backend/target/release
|
||||||
|
scp -r templates chris@$proxy:development/jean-marie/backend
|
||||||
|
|
||||||
Loading…
Reference in New Issue