├── database ├── Dockerfile ├── table │ ├── ip_address.sql │ ├── person_ip_address.sql │ ├── posession.sql │ ├── term.sql │ ├── skill.sql │ ├── password.sql │ ├── outcome.sql │ ├── feature.sql │ ├── goal.sql │ ├── action.sql │ ├── dictionary.sql │ ├── web_authentication_login.sql │ ├── schedule.sql │ ├── web_authentication_register.sql │ ├── person_goal.sql │ ├── dictionary_term.sql │ ├── person_skill.sql │ ├── web_authentication_key.sql │ ├── person_password.sql │ ├── person_principle.sql │ ├── person_quick_task.sql │ ├── sleep_schedule.sql │ ├── person_dictionary.sql │ ├── person_action.sql │ ├── person_email_address.sql │ ├── person_sleep_schedule.sql │ ├── principle.sql │ ├── person_web_authentication_key.sql │ ├── chronicle.sql │ ├── intention_instance.sql │ ├── chronicle_document.sql │ ├── schedule_weekday.sql │ ├── person_intention_schedule.sql │ ├── person.sql │ ├── person_chronicle.sql │ ├── intention.sql │ └── email_address.sql ├── configuration │ ├── create databases.sql │ ├── install_extensions.sql │ ├── update database roles.sql │ └── create database roles.sql ├── README.md ├── query │ ├── current chronicle query.sql │ └── person │ │ └── by_email_address.sql ├── enum │ ├── weekday.sql │ └── frequency.sql ├── .project ├── docker-compose.yml └── initalization │ └── setup.sql ├── documentation ├── entity │ ├── schedule.md │ ├── entity_template.md │ ├── intention.md │ ├── intention_status.md │ ├── outcome.md │ ├── person_intention.md │ ├── document.md │ ├── chronicle.md │ ├── action.md │ ├── person.md │ ├── lore.md │ └── chronicle │ │ └── feature │ │ └── chronicled_actions.md ├── name.md ├── data storage.md ├── openapi │ └── chronicle │ │ └── openapi.yml ├── README.md ├── features │ ├── thinking.md │ ├── challenges.md │ └── action_planning.md ├── implementation │ ├── authentication.md │ └── password_encryption.md ├── tracking │ └── work_session.md ├── user story.md └── features.md ├── loremaster-web-interface ├── src │ ├── data │ │ ├── entity │ │ │ ├── value.rs │ │ │ ├── person_chronicle.rs │ │ │ ├── sleep_schedule.rs │ │ │ ├── intention.rs │ │ │ ├── person_meta.rs │ │ │ ├── goal.rs │ │ │ ├── frequency.rs │ │ │ ├── webauthn.rs │ │ │ └── action.rs │ │ ├── interface.rs │ │ ├── entity.rs │ │ └── interface │ │ │ └── interface_datum.rs │ ├── components │ │ ├── widget │ │ │ ├── calendar │ │ │ │ ├── month.rs │ │ │ │ └── week.rs │ │ │ ├── navigation.rs │ │ │ ├── data │ │ │ │ ├── form │ │ │ │ │ └── add_value.rs │ │ │ │ ├── list.rs │ │ │ │ ├── form.rs │ │ │ │ └── list │ │ │ │ │ └── value_list.rs │ │ │ ├── data.rs │ │ │ ├── calendar.rs │ │ │ ├── notification.rs │ │ │ ├── notification │ │ │ │ ├── alert.rs │ │ │ │ └── toast.rs │ │ │ ├── theme_toggle.rs │ │ │ ├── frequency_options.rs │ │ │ ├── goal_list.rs │ │ │ └── date_time.rs │ │ ├── form.rs │ │ ├── navigation │ │ │ ├── tab.rs │ │ │ ├── tab │ │ │ │ ├── tab_panel.rs │ │ │ │ ├── tab_section.rs │ │ │ │ └── tab_button.rs │ │ │ ├── top_nav_bar.rs │ │ │ └── side_nav_bar.rs │ │ ├── state │ │ │ ├── validation.rs │ │ │ ├── visibility.rs │ │ │ └── message_type.rs │ │ ├── state.rs │ │ ├── widget.rs │ │ ├── popover.rs │ │ ├── information.rs │ │ ├── switch.rs │ │ ├── container.rs │ │ ├── form │ │ │ └── input_validation.rs │ │ ├── accordion.rs │ │ ├── navigation.rs │ │ └── combobox.rs │ ├── data.rs │ ├── utility.rs │ ├── templates.rs │ ├── components.rs │ ├── templates │ │ └── about.rs │ ├── utility │ │ ├── date_time_helper.rs │ │ └── constants.rs │ └── main.rs ├── .cargo │ └── config.toml ├── .gitignore ├── static │ └── favicon_io │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── site.webmanifest │ │ └── about.txt ├── .vscode │ └── settings.json ├── README.md └── Cargo.toml ├── loremaster-web-server ├── src │ ├── data │ │ ├── validation.rs │ │ ├── entity │ │ │ ├── frequency.rs │ │ │ ├── transfer │ │ │ │ ├── chroncile_entry.rs │ │ │ │ └── person_chronicle.rs │ │ │ ├── transfer.rs │ │ │ ├── holiday.rs │ │ │ ├── weekday.rs │ │ │ ├── action.rs │ │ │ ├── passkey.rs │ │ │ ├── quick_task.rs │ │ │ ├── sleep_schedule.rs │ │ │ ├── web_authentication_key.rs │ │ │ ├── schedule.rs │ │ │ ├── email_address.rs │ │ │ ├── web_authentication_challenge.rs │ │ │ ├── goal.rs │ │ │ ├── intention.rs │ │ │ └── person.rs │ │ ├── query │ │ │ ├── frequency.rs │ │ │ ├── goal │ │ │ │ ├── get_goal_by_id.rs │ │ │ │ ├── get_goals_by_person_id.rs │ │ │ │ ├── get_goal_by_name.rs │ │ │ │ ├── create_goal.rs │ │ │ │ └── get_goal_list.rs │ │ │ ├── password.rs │ │ │ ├── email_address.rs │ │ │ ├── sleep_schedule.rs │ │ │ ├── action.rs │ │ │ ├── goal.rs │ │ │ ├── authentication.rs │ │ │ ├── authentication │ │ │ │ ├── remove_stale_web_authentication_login_by_user_name.rs │ │ │ │ ├── remove_stale_web_authentication_challenges_by_user_name.rs │ │ │ │ ├── get_web_authentication_login.rs │ │ │ │ ├── get_web_authentication_by_user_name.rs │ │ │ │ ├── get_web_authentication_register.rs │ │ │ │ ├── add_web_authentication_key.rs │ │ │ │ ├── add_web_authentication_login.rs │ │ │ │ ├── add_web_authentication_register.rs │ │ │ │ └── get_webauthn_passkeys_by_email_address.rs │ │ │ ├── action │ │ │ │ ├── get_all_actions.rs │ │ │ │ ├── get_action_by_name.rs │ │ │ │ └── create_action.rs │ │ │ ├── password │ │ │ │ └── add_password.rs │ │ │ ├── person │ │ │ │ ├── add_password.rs │ │ │ │ ├── alias_by_id.rs │ │ │ │ ├── meta_by_id.rs │ │ │ │ ├── update_email_address.rs │ │ │ │ ├── goal_is_related.rs │ │ │ │ ├── add_web_authentication_key.rs │ │ │ │ ├── add_goal.rs │ │ │ │ ├── action_is_related.rs │ │ │ │ ├── remove_one_goal.rs │ │ │ │ ├── add_action.rs │ │ │ │ ├── get_person_sleep_schedule.rs │ │ │ │ ├── update_person_sleep_schedule.rs │ │ │ │ ├── person_by_email_address.rs │ │ │ │ ├── credential_by_email_address.rs │ │ │ │ ├── create_person.rs │ │ │ │ └── update_meta_by_id.rs │ │ │ ├── chronicle │ │ │ │ ├── chronicle_by_id.rs │ │ │ │ ├── chronicle_by_date.rs │ │ │ │ ├── current_chronicle_by_person.rs │ │ │ │ └── create_chronicle.rs │ │ │ ├── sleep_schedule │ │ │ │ ├── get_sleep_schedule_by_time.rs │ │ │ │ └── create_sleep_schedule.rs │ │ │ ├── email_address │ │ │ │ ├── email_address_in_use.rs │ │ │ │ └── create_email_address.rs │ │ │ └── person.rs │ │ ├── query.rs │ │ ├── entity.rs │ │ ├── surreal_handler.rs │ │ └── postgres_handler.rs │ ├── api │ │ ├── guards.rs │ │ ├── handler.rs │ │ ├── router.rs │ │ ├── response.rs │ │ ├── guards │ │ │ └── user.rs │ │ └── handler │ │ │ └── chronicle.rs │ ├── security │ │ ├── authentication │ │ │ └── web_authentication_api.rs │ │ ├── authentication.rs │ │ └── sanitization.rs │ ├── configuration.rs │ ├── security.rs │ ├── utility.rs │ ├── api.rs │ ├── utility │ │ ├── constants │ │ │ ├── cookie_fields.rs │ │ │ ├── unicode.rs │ │ │ ├── database.rs │ │ │ └── files.rs │ │ ├── scheduler.rs │ │ └── constants.rs │ ├── data.rs │ ├── configuration │ │ └── logging.rs │ └── main.rs ├── .gitignore ├── .idea │ ├── vcs.xml │ ├── .gitignore │ ├── modules.xml │ └── loremaster-web-server.iml ├── Loremaster-template.ron ├── Dockerfile ├── requests.http ├── README.md └── Cargo.toml ├── loremaster ├── src │ ├── data.rs │ ├── lib.rs │ └── data │ │ └── postgres_handler.rs └── Cargo.toml ├── .idea ├── vcs.xml ├── .gitignore ├── modules.xml └── loremaster.iml ├── Cargo.toml ├── .vscode ├── launch.json └── settings.json ├── infrastructure └── google_cloud_platform │ ├── terraform │ ├── main.tf │ └── .terraform.lock.hcl │ └── README.md ├── .github ├── workflows │ ├── web_interface_build.yaml │ ├── web_server_build.yml │ └── rust-clippy.yml └── ISSUE_TEMPLATE │ └── feature.md ├── .gitignore ├── loremaster-database ├── Cargo.toml └── src │ └── main.rs └── README.md /database/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres -------------------------------------------------------------------------------- /database/table/ip_address.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /documentation/entity/schedule.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/table/person_ip_address.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/entity/value.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/validation.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /loremaster/src/data.rs: -------------------------------------------------------------------------------- 1 | pub mod postgres_handler; 2 | -------------------------------------------------------------------------------- /loremaster-web-server/src/api/guards.rs: -------------------------------------------------------------------------------- 1 | pub mod user; 2 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/frequency.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/frequency.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/goal/get_goal_by_id.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/calendar/month.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/navigation.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/transfer/chroncile_entry.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/goal/get_goals_by_person_id.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/data/form/add_value.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/form.rs: -------------------------------------------------------------------------------- 1 | pub mod input_validation; 2 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/interface.rs: -------------------------------------------------------------------------------- 1 | pub mod interface_datum; 2 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/password.rs: -------------------------------------------------------------------------------- 1 | pub mod add_password; 2 | -------------------------------------------------------------------------------- /loremaster-web-server/src/security/authentication/web_authentication_api.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/data/list.rs: -------------------------------------------------------------------------------- 1 | pub mod value_list; 2 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data.rs: -------------------------------------------------------------------------------- 1 | pub mod entity; 2 | pub mod interface; 3 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/transfer.rs: -------------------------------------------------------------------------------- 1 | pub mod person_chronicle; 2 | -------------------------------------------------------------------------------- /database/configuration/create databases.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE loremaster_dev_1 OWNER naes; -------------------------------------------------------------------------------- /database/configuration/install_extensions.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -------------------------------------------------------------------------------- /loremaster-web-server/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .perseus/ 3 | certs/*.pem 4 | 5 | .DS_Store -------------------------------------------------------------------------------- /loremaster-web-server/src/configuration.rs: -------------------------------------------------------------------------------- 1 | pub mod application; 2 | pub mod logging; 3 | -------------------------------------------------------------------------------- /loremaster-web-server/src/security/authentication.rs: -------------------------------------------------------------------------------- 1 | pub mod web_authentication_api; 2 | -------------------------------------------------------------------------------- /loremaster-web-interface/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["--cfg", "engine"] 3 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/data.rs: -------------------------------------------------------------------------------- 1 | pub mod form; 2 | pub mod list; 3 | -------------------------------------------------------------------------------- /loremaster-web-server/src/security.rs: -------------------------------------------------------------------------------- 1 | pub mod authentication; 2 | pub mod sanitization; 3 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/calendar.rs: -------------------------------------------------------------------------------- 1 | pub mod month; 2 | pub mod week; 3 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/notification.rs: -------------------------------------------------------------------------------- 1 | pub mod alert; 2 | pub mod toast; 3 | -------------------------------------------------------------------------------- /documentation/name.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | - Chronilore 4 | - Loremaster 5 | - Schedulore 6 | - Rigmarole 7 | -------------------------------------------------------------------------------- /loremaster-web-server/src/api/handler.rs: -------------------------------------------------------------------------------- 1 | pub mod authentication; 2 | pub mod chronicle; 3 | pub mod person; 4 | -------------------------------------------------------------------------------- /loremaster-web-server/src/api/router.rs: -------------------------------------------------------------------------------- 1 | pub mod authentication; 2 | pub mod chronicle; 3 | pub mod person; 4 | -------------------------------------------------------------------------------- /loremaster-web-interface/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .perseus/ 3 | static/bootstrap*/ 4 | static/icons/ 5 | 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/utility.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod date_time_helper; 3 | pub mod http_service; 4 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/email_address.rs: -------------------------------------------------------------------------------- 1 | pub mod create_email_address; 2 | pub mod email_address_in_use; 3 | -------------------------------------------------------------------------------- /loremaster-web-server/src/utility.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | 3 | pub mod password_encryption; 4 | pub mod scheduler; 5 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/navigation/tab.rs: -------------------------------------------------------------------------------- 1 | pub mod tab_button; 2 | pub mod tab_panel; 3 | pub mod tab_section; 4 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/sleep_schedule.rs: -------------------------------------------------------------------------------- 1 | pub mod create_sleep_schedule; 2 | pub mod get_sleep_schedule_by_time; 3 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/action.rs: -------------------------------------------------------------------------------- 1 | pub mod create_action; 2 | pub mod get_action_by_name; 3 | pub mod get_all_actions; 4 | -------------------------------------------------------------------------------- /database/README.md: -------------------------------------------------------------------------------- 1 | # Loremaster Database 2 | 3 | Docker compose command 4 | 5 | ```sh 6 | docker-compose -f docker-compose.yml up 7 | ``` 8 | -------------------------------------------------------------------------------- /loremaster-web-server/src/api.rs: -------------------------------------------------------------------------------- 1 | pub mod guards; 2 | pub mod handler; 3 | pub mod response; 4 | pub mod router; 5 | pub mod web_server; 6 | -------------------------------------------------------------------------------- /loremaster-web-server/src/utility/constants/cookie_fields.rs: -------------------------------------------------------------------------------- 1 | pub const SESSION_ID: &str = "session_id"; 2 | pub const USER_ID: &str = "user_id"; 3 | -------------------------------------------------------------------------------- /database/table/posession.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public".possesion 2 | ( 3 | "id" uuid NOT NULL, 4 | CONSTRAINT PK_possesion PRIMARY KEY ( "id" ) 5 | ); 6 | -------------------------------------------------------------------------------- /database/table/term.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "term"( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "name" text NOT NULL UNIQUE 5 | ); 6 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/data/form.rs: -------------------------------------------------------------------------------- 1 | pub mod web_authentication_api_login; 2 | pub mod web_authentication_api_registration; 3 | -------------------------------------------------------------------------------- /database/table/skill.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "skill" ( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "name" text UNIQUE NOT NULL 5 | ); 6 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data.rs: -------------------------------------------------------------------------------- 1 | pub mod entity; 2 | pub mod postgres_handler; 3 | pub mod query; 4 | pub mod surreal_handler; 5 | pub mod validation; 6 | -------------------------------------------------------------------------------- /documentation/data storage.md: -------------------------------------------------------------------------------- 1 | # Data Storage 2 | 3 | ## Options 4 | 5 | - User stores data locally on devices 6 | - We store user's data remotely on servers 7 | -------------------------------------------------------------------------------- /database/table/password.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "password" ( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "encrypted_password" text NOT NULL 5 | ); 6 | -------------------------------------------------------------------------------- /loremaster-web-interface/static/favicon_io/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpmyers/loremaster/HEAD/loremaster-web-interface/static/favicon_io/favicon.ico -------------------------------------------------------------------------------- /database/table/outcome.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public".outcome 2 | ( 3 | "id" uuid NOT NULL, 4 | Name text NOT NULL, 5 | CONSTRAINT PK_outcome PRIMARY KEY ( "id" ) 6 | ); 7 | -------------------------------------------------------------------------------- /loremaster-web-interface/static/favicon_io/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpmyers/loremaster/HEAD/loremaster-web-interface/static/favicon_io/favicon-16x16.png -------------------------------------------------------------------------------- /loremaster-web-interface/static/favicon_io/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpmyers/loremaster/HEAD/loremaster-web-interface/static/favicon_io/favicon-32x32.png -------------------------------------------------------------------------------- /loremaster-web-interface/static/favicon_io/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpmyers/loremaster/HEAD/loremaster-web-interface/static/favicon_io/apple-touch-icon.png -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/goal.rs: -------------------------------------------------------------------------------- 1 | pub mod create_goal; 2 | pub mod get_goal_by_id; 3 | pub mod get_goal_by_name; 4 | pub mod get_goal_list; 5 | pub mod get_goals_by_person_id; 6 | -------------------------------------------------------------------------------- /database/table/feature.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "feature" ( 2 | "id" uuid NOT NULL UNIQUE PRIMARY KEY, 3 | "name" text NOT NULL UNIQUE, 4 | "creation_date" timestamp(0) with time zone NOT NULL 5 | ); 6 | -------------------------------------------------------------------------------- /loremaster-web-interface/static/favicon_io/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpmyers/loremaster/HEAD/loremaster-web-interface/static/favicon_io/android-chrome-192x192.png -------------------------------------------------------------------------------- /loremaster-web-interface/static/favicon_io/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanpmyers/loremaster/HEAD/loremaster-web-interface/static/favicon_io/android-chrome-512x512.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /database/query/current chronicle query.sql: -------------------------------------------------------------------------------- 1 | SELECT DISTINCT 2 | chronicle.id 3 | , chronicle.date_recorded 4 | FROM 5 | public.chronicle 6 | WHERE 7 | chronicle.date_recorded = CURRENT_DATE 8 | ; -------------------------------------------------------------------------------- /database/enum/weekday.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE weekday AS ENUM ( 2 | 'Sunday', 3 | 'Monday', 4 | 'Tuesday', 5 | 'Wednesday', 6 | 'Thursday', 7 | 'Friday', 8 | 'Saturday' 9 | ); 10 | -------------------------------------------------------------------------------- /database/table/goal.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "goal" ( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "name" text UNIQUE NOT NULL 5 | ); 6 | 7 | CREATE INDEX "goal_name_index" ON "goal" ("name"); 8 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/templates.rs: -------------------------------------------------------------------------------- 1 | pub mod about; 2 | pub mod chronicle; 3 | pub mod design_system; 4 | pub mod index; 5 | pub mod login; 6 | pub mod registration; 7 | pub mod timeline; 8 | pub mod you; 9 | -------------------------------------------------------------------------------- /database/query/person/by_email_address.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | email_address 3 | , encrypted_password 4 | FROM 5 | public.loremaster.person 6 | WHERE 7 | person.email_address = '$1' 8 | LIMIT 9 | 1 10 | ; -------------------------------------------------------------------------------- /database/table/action.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "action" ( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "name" text NOT NULL UNIQUE 5 | ); 6 | 7 | CREATE INDEX "action_name_index" ON "action" ("name"); 8 | -------------------------------------------------------------------------------- /database/table/dictionary.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "dictionary"( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "name" text NOT NULL UNIQUE, 5 | "user_made" boolean NOT NULL DEFAULT FALSE 6 | ); 7 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/state/validation.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Serialize, Debug, Clone)] 4 | pub enum Validation { 5 | Valid, 6 | Invalid, 7 | } 8 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/state/visibility.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Serialize, Debug, Clone)] 4 | pub enum Visibility { 5 | Visible, 6 | Hidden, 7 | } 8 | -------------------------------------------------------------------------------- /database/table/web_authentication_login.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "web_authentication_login" ( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "user_name" text NOT NULL, 5 | "passkey" jsonb NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /documentation/entity/entity_template.md: -------------------------------------------------------------------------------- 1 | # Title 2 | 3 | ## Description 4 | Description 5 | 6 | ## Attributes 7 | - Attribute1 8 | - Attribute2 9 | 10 | ## Related Entites 11 | 12 | - [Entity]() 13 | - [Entity2]() 14 | -------------------------------------------------------------------------------- /loremaster-web-interface/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "autoserde", 4 | "bindgen", 5 | "combobox", 6 | "gloo", 7 | "proto", 8 | "reqwasm", 9 | "webauthn", 10 | "YUBIKEY" 11 | ] 12 | } -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/state.rs: -------------------------------------------------------------------------------- 1 | pub mod message_type; 2 | pub mod validation; 3 | pub mod visibility; 4 | pub enum ComponentState { 5 | Error, 6 | Hidden, 7 | Loading, 8 | Visible, 9 | } 10 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/entity.rs: -------------------------------------------------------------------------------- 1 | pub mod action; 2 | pub mod frequency; 3 | pub mod goal; 4 | pub mod intention; 5 | pub mod person_chronicle; 6 | pub mod person_meta; 7 | pub mod sleep_schedule; 8 | pub mod webauthn; 9 | -------------------------------------------------------------------------------- /database/enum/frequency.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE frequency AS ENUM ( 2 | 'Day', 3 | 'Weekday', 4 | 'Week', 5 | 'Month', 6 | 'Year', 7 | 'Hour', 8 | 'Minute', 9 | 'Custom', 10 | 'Subsequent' 11 | ); 12 | -------------------------------------------------------------------------------- /database/table/schedule.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "schedule" ( 3 | "id" uuid NOT NULL UNIQUE PRIMARY KEY, 4 | "frequency_interval" interval NOT NULL, 5 | "subsequent" boolean NOT NULL DEFAULT FALSE 6 | ); 7 | -------------------------------------------------------------------------------- /database/table/web_authentication_register.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "web_authentication_register" ( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "user_name" text NOT NULL, 5 | "passkey" jsonb NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/holiday.rs: -------------------------------------------------------------------------------- 1 | // use chrono::{Date, Utc}; 2 | // use uuid::Uuid; 3 | 4 | // pub struct Holiday { 5 | // pub id: Uuid, 6 | // pub occurence_date: Date, 7 | // pub name: String, 8 | // } 9 | -------------------------------------------------------------------------------- /loremaster-web-server/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /documentation/entity/intention.md: -------------------------------------------------------------------------------- 1 | # Intention 2 | 3 | ## Description 4 | a thing planned or meant; an aim or plan. 5 | 6 | ## Attributes 7 | - Name 8 | 9 | ## Related Entites 10 | 11 | - [Person Intention](./person_intention.md) 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["loremaster", "loremaster-database", "loremaster-web-server"] 3 | default-members = ["loremaster", "loremaster-database", "loremaster-web-server"] 4 | exclude = ["loremaster-web-interface"] 5 | resolver = "2" 6 | -------------------------------------------------------------------------------- /documentation/entity/intention_status.md: -------------------------------------------------------------------------------- 1 | # Intention Status 2 | 3 | ## Description 4 | The status of an intention held by a person. 5 | 6 | ## Attributes 7 | - Name 8 | 9 | ## Related Entites 10 | 11 | - [Person Intention](./person_intention.md) -------------------------------------------------------------------------------- /loremaster-web-server/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query.rs: -------------------------------------------------------------------------------- 1 | pub mod action; 2 | pub mod authentication; 3 | pub mod chronicle; 4 | pub mod email_address; 5 | pub mod frequency; 6 | pub mod goal; 7 | pub mod password; 8 | pub mod person; 9 | pub mod sleep_schedule; 10 | -------------------------------------------------------------------------------- /database/table/person_goal.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person_goal" ( 3 | "person_id" uuid NOT NULL REFERENCES "person" ("id"), 4 | "goal_id" uuid NOT NULL REFERENCES "goal" ("id"), 5 | PRIMARY KEY ("person_id", "goal_id") 6 | ); 7 | -------------------------------------------------------------------------------- /database/table/dictionary_term.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "dictionary_term"( 3 | "dictionary_id" uuid NOT NULL REFERENCES "dictionary", 4 | "term_id" uuid NOT NULL REFERENCES "term", 5 | PRIMARY KEY ("dictionary_id", "term_id") 6 | ); 7 | -------------------------------------------------------------------------------- /database/table/person_skill.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person_skill" ( 3 | "person_id" uuid NOT NULL REFERENCES "person" ("id"), 4 | "skill_id" uuid NOT NULL REFERENCES "skill" ("id"), 5 | PRIMARY KEY ("person_id", "skill_id") 6 | ); 7 | -------------------------------------------------------------------------------- /database/table/web_authentication_key.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "web_authentication_key" ( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "credential_id" bytea NOT NULL, 5 | "cose_algorithm" int NOT NULL, 6 | "passkey" jsonb NOT NULL 7 | ); 8 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget.rs: -------------------------------------------------------------------------------- 1 | pub mod calendar; 2 | pub mod data; 3 | pub mod date_time; 4 | pub mod frequency_options; 5 | pub mod goal_list; 6 | pub mod navigation; 7 | pub mod notification; 8 | pub mod sleep; 9 | pub mod theme_toggle; 10 | -------------------------------------------------------------------------------- /database/table/person_password.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person_password" ( 3 | "person_id" uuid NOT NULL REFERENCES "person" ("id"), 4 | "password_id" uuid NOT NULL REFERENCES "password" ("id"), 5 | PRIMARY KEY ("person_id", "password_id") 6 | ); 7 | -------------------------------------------------------------------------------- /database/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | database 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /database/table/person_principle.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person_principle" ( 3 | "person_id" uuid NOT NULL REFERENCES "person" ("id"), 4 | "principle_id" uuid NOT NULL REFERENCES "principle" ("id"), 5 | PRIMARY KEY ("person_id", "principle_id") 6 | ); 7 | -------------------------------------------------------------------------------- /database/table/person_quick_task.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person_quick_task"( 3 | "person_id" uuid NOT NULL REFERENCES "person" ("id"), 4 | "quick_task_id" uuid NOT NULL REFERENCES "quick_task" ("id"), 5 | PRIMARY KEY ("person_id", "quick_task_id") 6 | ); 7 | -------------------------------------------------------------------------------- /database/table/sleep_schedule.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "sleep_schedule" ( 2 | "id" uuid NOT NULL, 3 | "start_time" time(0) without time zone NOT NULL, 4 | "end_time" time(0) without time zone NOT NULL 5 | ); 6 | 7 | ALTER TABLE "sleep_schedule" 8 | ADD PRIMARY KEY ("id"); 9 | -------------------------------------------------------------------------------- /database/table/person_dictionary.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person_dictionary" ( 3 | "person_id" uuid NOT NULL REFERENCES "person" ("id"), 4 | "dictionary_id" uuid NOT NULL REFERENCES "dictionary" ("id"), 5 | PRIMARY KEY ("person_id", "dictionary_id") 6 | ); 7 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components.rs: -------------------------------------------------------------------------------- 1 | pub mod accordion; 2 | pub mod combobox; 3 | pub mod container; 4 | pub mod form; 5 | pub mod icon; 6 | pub mod information; 7 | pub mod modal; 8 | pub mod navigation; 9 | pub mod popover; 10 | pub mod state; 11 | pub mod switch; 12 | pub mod widget; 13 | -------------------------------------------------------------------------------- /database/table/person_action.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person_action" ( 3 | "action_id" uuid NOT NULL REFERENCES "action" ("id"), 4 | "person_id" uuid NOT NULL REFERENCES "person" ("id"), 5 | "average_duration" interval NULL, 6 | PRIMARY KEY ("action_id", "person_id") 7 | ); 8 | -------------------------------------------------------------------------------- /loremaster-web-interface/static/favicon_io/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /loremaster/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod data; 2 | 3 | pub fn add(left: usize, right: usize) -> usize { 4 | left + right 5 | } 6 | 7 | #[cfg(test)] 8 | mod tests { 9 | use super::*; 10 | 11 | #[test] 12 | fn it_works() { 13 | let result = add(2, 2); 14 | assert_eq!(result, 4); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /loremaster-web-server/src/utility/constants/unicode.rs: -------------------------------------------------------------------------------- 1 | // Invisible Unicode characters 2 | pub mod invisible { 3 | pub const _CHARACTER_TABULATION: char = '\u{00009}'; 4 | // pub const SPACE: char = '\u{0020}'; 5 | // pub const NO_BREAK_SPACE: char = '\u{00A0}'; 6 | pub const _LEFT_TO_RIGHT_MARK: char = '\u{200E}'; 7 | } 8 | -------------------------------------------------------------------------------- /documentation/entity/outcome.md: -------------------------------------------------------------------------------- 1 | # Outcome 2 | 3 | ## Description 4 | The way a thing turns out; a consequence. 5 | The result of a person's action(s). 6 | 7 | ## Attributes 8 | - Name 9 | - Description (?) 10 | 11 | ## Related Entites 12 | 13 | - [Action](./action.md) 14 | - [Person](./person.md) 15 | - [Chronicle](./chronicle.md) -------------------------------------------------------------------------------- /documentation/entity/person_intention.md: -------------------------------------------------------------------------------- 1 | # Title 2 | 3 | ## Description 4 | An intention taken up or held by a person. 5 | 6 | ## Attributes 7 | - Person 8 | - Set on 9 | - Status 10 | 11 | ## Related Entites 12 | 13 | - [Person](./person.md) 14 | - [Intention](./intention.md) 15 | - [Intention Status](./intention_status.md) 16 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/entity/person_chronicle.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use time::Date; 3 | use uuid::Uuid; 4 | 5 | #[derive(Deserialize, Serialize, Debug, Clone)] 6 | pub struct PersonChronicle { 7 | pub chronicle_id: Uuid, 8 | pub chronicle_date: Date, 9 | pub person_alias: Option, 10 | } 11 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/interface/interface_datum.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | 3 | pub trait InterfaceDatum { 4 | type InterfaceId; 5 | type InterfaceDisplayText; 6 | type InterfaceDescription; 7 | } 8 | 9 | pub type InterfaceDisplayText = String; 10 | pub type InterfaceId = Uuid; 11 | pub type InterfaceDescription = String; 12 | -------------------------------------------------------------------------------- /database/table/person_email_address.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person_email_address" ( 3 | "person_id" uuid NOT NULL REFERENCES "person" ("id"), 4 | "email_address_id" uuid NOT NULL REFERENCES "email_address" ("id"), 5 | PRIMARY KEY ( 6 | "person_id", 7 | "email_address_id" 8 | ) 9 | ); 10 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/entity/sleep_schedule.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use time::Time; 3 | use uuid::Uuid; 4 | 5 | #[derive(Deserialize, Serialize, Debug, Clone)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct SleepSchedule { 8 | pub id: Uuid, 9 | pub start_time: Time, 10 | pub end_time: Time, 11 | } 12 | -------------------------------------------------------------------------------- /loremaster-web-server/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /database/table/person_sleep_schedule.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person_sleep_schedule" ( 3 | "person_id" uuid NOT NULL UNIQUE REFERENCES "person" ("id"), 4 | "sleep_schedule_id" uuid NOT NULL REFERENCES "sleep_schedule" ("id"), 5 | PRIMARY KEY ( 6 | "person_id", 7 | "sleep_schedule_id" 8 | ) 9 | ); 10 | -------------------------------------------------------------------------------- /loremaster-web-server/.idea/loremaster-web-server.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /database/table/principle.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "principle" ( 2 | "id" uuid NOT NULL, 3 | "name" text NOT NULL, 4 | "reason" text 5 | ); 6 | 7 | CREATE INDEX "principle_name_index" ON "principle" ("name"); 8 | 9 | ALTER TABLE "principle" 10 | ADD PRIMARY KEY ("id"); 11 | 12 | ALTER TABLE "principle" 13 | ADD CONSTRAINT "principle_name_unique" UNIQUE ("name"); 14 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/entity/intention.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 5 | pub struct ChronicleIntention { 6 | pub intention_id: Uuid, 7 | pub chronicle_id: Uuid, 8 | pub person_id: Uuid, 9 | pub action_id: Uuid, 10 | pub action: String, 11 | } 12 | -------------------------------------------------------------------------------- /database/table/person_web_authentication_key.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person_web_authentication_key" ( 3 | "person_id" uuid NOT NULL REFERENCES "person"("id"), 4 | "web_authentication_key_id" uuid NOT NULl REFERENCES "web_authentication_key"("id"), 5 | PRIMARY KEY ( 6 | "person_id", 7 | "web_authentication_key_id" 8 | ) 9 | ); 10 | -------------------------------------------------------------------------------- /documentation/openapi/chronicle/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.2" 2 | info: 3 | title: Chronicle 4 | description: | 5 | This API provides chronicle data for loremaster users 6 | version: "1.0" 7 | servers: 8 | - url: https://loremaster.chronilore.day/api/v1 9 | paths: 10 | /chronicle/today: 11 | get: 12 | responses: 13 | "200": 14 | description: OK 15 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/transfer/person_chronicle.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use time::Date; 3 | 4 | use crate::data::entity::chronicle::ChronicleId; 5 | 6 | #[derive(Deserialize, Serialize, Debug, Clone)] 7 | pub struct PersonChronicle { 8 | pub chronicle_id: ChronicleId, 9 | pub chronicle_date: Date, 10 | pub person_alias: Option, 11 | } 12 | -------------------------------------------------------------------------------- /database/table/chronicle.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "chronicle" ( 3 | "id" uuid NOT NULL UNIQUE, 4 | "person_id" uuid NOT NULL REFERENCES "person" ("id"), 5 | "date_recorded" date NOT NULL UNIQUE, 6 | "notes" text NULL, 7 | "creation_time" timestamp(0) 8 | with 9 | time zone NULL, 10 | PRIMARY KEY ("id", "date_recorded") 11 | ); 12 | -------------------------------------------------------------------------------- /database/table/intention_instance.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "intention_instance" ( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "intention_id" uuid NOT NULL REFERENCES "intention" ("id"), 5 | "date_and_time" timestamp(0) 6 | WITH 7 | time zone NOT NULL, 8 | "complete" boolean NOT NULL DEFAULT FALSE, 9 | "duration" interval NULL 10 | ); 11 | -------------------------------------------------------------------------------- /loremaster-web-server/src/utility/constants/database.rs: -------------------------------------------------------------------------------- 1 | // pub const ID: &str = "id"; 2 | // pub const EMAIL_ADDRESS: &str = "email_address"; 3 | // pub const ENCRYPTED_PASSWORD: &str = "encrypted_password"; 4 | pub const _REGISTRATION_DATE: &str = "registration_date"; 5 | pub const _ALIAS: &str = "alias"; 6 | pub const _CHRONICLE_ID: &str = "chronicle_id"; 7 | // pub const DATE_RECORDED: &str = "date_recorded"; 8 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/weekday.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | // These are defined in SQL, this should match what is there 4 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::Type, PartialEq)] 5 | #[sqlx(type_name = "weekday")] 6 | pub enum Weekday { 7 | Sunday, 8 | Monday, 9 | Tuesday, 10 | Wednesday, 11 | Thursday, 12 | Friday, 13 | Saturday, 14 | } 15 | -------------------------------------------------------------------------------- /loremaster-web-server/src/utility/constants/files.rs: -------------------------------------------------------------------------------- 1 | // pub const INDEX_PATH: &str = "frontend/dist/index.html"; 2 | // pub const FAVICON_PATH: &str = "./frontend/resources/favicon_io/favicon.ico"; 3 | pub const FRONTEND_DIST_PATH: &str = "../loremaster-web-interface/dist/exported"; 4 | pub const SSL_CERT_PATH: &str = "./certs"; 5 | pub const SSL_CERT_FILE_PATH: &str = "cert.pem"; 6 | pub const SSL_CERT_KEY_FILE_PATH: &str = "key.pem"; 7 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity.rs: -------------------------------------------------------------------------------- 1 | pub mod action; 2 | pub mod chronicle; 3 | pub mod email_address; 4 | pub mod frequency; 5 | pub mod goal; 6 | pub mod holiday; 7 | pub mod intention; 8 | pub mod passkey; 9 | pub mod person; 10 | pub mod quick_task; 11 | pub mod schedule; 12 | pub mod sleep_schedule; 13 | pub mod transfer; 14 | pub mod web_authentication_challenge; 15 | pub mod web_authentication_key; 16 | pub mod weekday; 17 | -------------------------------------------------------------------------------- /loremaster-web-interface/static/favicon_io/about.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following graphics from Twitter Twemoji: 2 | 3 | - Graphics Title: 262f.svg 4 | - Graphics Author: Copyright 2020 Twitter, Inc and other contributors (https://github.com/twitter/twemoji) 5 | - Graphics Source: https://github.com/twitter/twemoji/blob/master/assets/svg/262f.svg 6 | - Graphics License: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/) 7 | -------------------------------------------------------------------------------- /documentation/entity/document.md: -------------------------------------------------------------------------------- 1 | # Document 2 | 3 | ## Definition 4 | 5 | > a body of traditions and knowledge on a subject or held by a particular group, typically passed from person to person by word of mouth. 6 | 7 | ## Attributes 8 | 9 | - Type 10 | - Category 11 | - Owner 12 | - Tags (?) 13 | - Sections (?) 14 | 15 | ## Related Entities 16 | 17 | - [Document Category](./document_category.md) 18 | - [Document Type](./document_type.md) 19 | - [Person](./person.md) 20 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/entity/person_meta.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use time::OffsetDateTime; 3 | use uuid::Uuid; 4 | 5 | #[derive(Deserialize, Serialize, Debug, Clone)] 6 | pub struct PersonMeta { 7 | pub id: Uuid, 8 | #[serde(rename(deserialize = "emailAddress"))] 9 | pub email_address: String, 10 | #[serde(rename(deserialize = "registrationDate"))] 11 | pub registration_date: OffsetDateTime, 12 | pub alias: Option, 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "rust:debug", 11 | "program": "${workspaceFolder}/", 12 | "args": [], 13 | "cwd": "${workspaceFolder}" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /database/table/chronicle_document.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public".chronicle_document 2 | ( 3 | chronicle_id uuid NOT NULL, 4 | document_id uuid NOT NULL, 5 | date_recorded date NOT NULL, 6 | CONSTRAINT PK_chronicle_document PRIMARY KEY ( chronicle_id, document_id, date_recorded ) 7 | ); 8 | 9 | CREATE INDEX fkIdx_214 ON "public".chronicle_document 10 | ( 11 | chronicle_id, 12 | date_recorded 13 | ); 14 | 15 | CREATE INDEX fkIdx_218 ON "public".chronicle_document 16 | ( 17 | document_id 18 | ); -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/authentication.rs: -------------------------------------------------------------------------------- 1 | pub mod add_web_authentication_key; 2 | pub mod add_web_authentication_login; 3 | pub mod add_web_authentication_register; 4 | pub mod get_web_authentication_by_user_name; 5 | pub mod get_web_authentication_login; 6 | pub mod get_web_authentication_register; 7 | pub mod get_webauthn_passkeys_by_email_address; 8 | pub mod remove_stale_web_authentication_challenges_by_user_name; 9 | pub mod remove_stale_web_authentication_login_by_user_name; 10 | -------------------------------------------------------------------------------- /infrastructure/google_cloud_platform/terraform/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | google = { 4 | source = "hashicorp/google" 5 | version = "3.5.0" 6 | } 7 | } 8 | } 9 | 10 | provider "google" { 11 | credentials = file("tc.gcp-service-account.json") 12 | 13 | project = "theta-carving-360312" 14 | region = "us-central1" 15 | zone = "us-central1-c" 16 | } 17 | 18 | resource "google_compute_network" "vpc_network" { 19 | name = "terraform-network" 20 | } 21 | -------------------------------------------------------------------------------- /loremaster-web-server/Loremaster-template.ron: -------------------------------------------------------------------------------- 1 | LoremasterWebServerConfiguration( 2 | environment: Local, 3 | scheduler_state: Off, 4 | web_server: WebServer( 5 | ipv4_address: (127, 0, 0, 1), 6 | port: 8000, 7 | ), 8 | database: Database( 9 | postgresql_connection_string: "postgres://postgres:postgres@localhost/postgres", 10 | ), 11 | encryption: Encryption( 12 | hash_iterations: 2, 13 | site_secret: "replace with a secret", 14 | ), 15 | front_end: FrontEnd( 16 | port: 8080 17 | ), 18 | ) -------------------------------------------------------------------------------- /database/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | db: 4 | container_name: loremaster_database 5 | image: postgres:alpine 6 | restart: always 7 | environment: 8 | - POSTGRES_USER=postgres 9 | - POSTGRES_PASSWORD=postgres 10 | ports: 11 | - '5432:5432' 12 | volumes: 13 | # - db:/var/lib/postgresql/data 14 | - ../database/initalization/:/docker-entrypoint-initdb.d 15 | - ../database:/database 16 | volumes: 17 | db: 18 | driver: local 19 | -------------------------------------------------------------------------------- /database/table/schedule_weekday.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "schedule_weekday"( 3 | "id" uuid NOT NULL UNIQUE PRIMARY KEY, 4 | "sunday" boolean NOT NULL DEFAULT TRUE, 5 | "monday" boolean NOT NULL DEFAULT TRUE, 6 | "tuesday" boolean NOT NULL DEFAULT TRUE, 7 | "wednesday" boolean NOT NULL DEFAULT TRUE, 8 | "thursday" boolean NOT NULL DEFAULT TRUE, 9 | "friday" boolean NOT NULL DEFAULT TRUE, 10 | "saturday" boolean NOT NULL DEFAULT TRUE 11 | ); 12 | -------------------------------------------------------------------------------- /loremaster/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "loremaster" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | # Error handling 10 | anyhow = "1.0.72" 11 | # Database client/pool/toolkit 12 | sqlx = { version = "0.6.3", features = [ 13 | "json", 14 | "macros", 15 | "migrate", 16 | # "offline", 17 | "postgres", 18 | "runtime-tokio-native-tls", 19 | "time", 20 | "tls", 21 | "uuid", 22 | ] } 23 | -------------------------------------------------------------------------------- /database/table/person_intention_schedule.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person_intention_schedule" ( 3 | "person_id" uuid NOT NULL REFERENCES "person" ("id"), 4 | "intention_id" uuid NOT NULL REFERENCES "intention" ("id"), 5 | "schedule_id" uuid NOT NULL REFERENCES "schedule" ("id"), 6 | "schedule_weekday_id" uuid NOT NULL REFERENCES "schedule_weekday" ("id"), 7 | PRIMARY KEY ( 8 | "person_id", 9 | "intention_id", 10 | "schedule_id" 11 | ) 12 | ); 13 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/entity/goal.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct Goal { 7 | pub id: Uuid, 8 | pub name: String, 9 | } 10 | 11 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct PersonGoal { 14 | pub person_id: Uuid, 15 | pub goal_id: Uuid, 16 | pub goal_name: String, 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/web_interface_build.yaml: -------------------------------------------------------------------------------- 1 | name: Web Interface Build 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | working-directory: loremaster-web-interface 22 | #- name: Run tests 23 | # run: cargo test --verbose 24 | -------------------------------------------------------------------------------- /database/table/person.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "person" ( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "email_address_id" uuid NOT NULL UNIQUE REFERENCES "email_address" ("id"), 5 | "registration_date" timestamp(0) 6 | with 7 | time zone NOT NULL, 8 | "alias" text NULL, 9 | "chronicle_id" uuid NULL UNIQUE 10 | ); 11 | 12 | CREATE INDEX "person_alias_index" ON "person" ("alias"); 13 | 14 | CREATE INDEX 15 | "person_registration_date_index" ON "person" ("registration_date"); 16 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/entity/frequency.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | // These are defined in SQL, this should match what is there 6 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 7 | pub enum Frequency { 8 | Day, 9 | Week, 10 | Weekday, 11 | Month, 12 | Year, 13 | Hour, 14 | Minute, 15 | } 16 | 17 | impl fmt::Display for Frequency { 18 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 19 | write!(f, "{:?}", self) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/entity/webauthn.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use webauthn_rs_proto::{PublicKeyCredential, RegisterPublicKeyCredential}; 3 | 4 | #[derive(Debug, Clone, Deserialize, Serialize)] 5 | pub struct WebAuthenticationInput { 6 | pub email_address: String, 7 | pub user_credential_json: RegisterPublicKeyCredential, 8 | } 9 | 10 | #[derive(Serialize, Deserialize, Debug)] 11 | pub struct PersonPublicKeyCredential { 12 | pub email_address: String, 13 | pub public_key_credential: PublicKeyCredential, 14 | } 15 | -------------------------------------------------------------------------------- /database/table/person_chronicle.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | public.person_chronicle ( 3 | person_id uuid NOT NULL REFERENCES "person" ("id"), 4 | chronicle_id uuid NOT NULL REFERENCES "chronicle" ("id"), 5 | date_recorded date NOT NULL, 6 | PRIMARY KEY ( 7 | person_id, 8 | date_recorded, 9 | chronicle_id 10 | ) 11 | ); 12 | 13 | CREATE INDEX 14 | "chronicle_date_index" ON "email_address" ("date_recorded"); 15 | 16 | CREATE INDEX 17 | "chronicle_person_index" ON "email_address" ("person_id"); 18 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/action.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 5 | pub struct Action { 6 | pub id: ActionId, 7 | pub name: String, 8 | } 9 | 10 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::Decode, sqlx::Encode)] 11 | pub struct ActionId(pub Uuid); 12 | 13 | impl sqlx::Type for ActionId { 14 | fn type_info() -> ::TypeInfo { 15 | >::type_info() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /database/table/intention.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "intention" ( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "action_id" uuid NOT NULL REFERENCES "action" ("id"), 5 | "person_id" uuid NOT NULL REFERENCES "person" ("id"), 6 | "created_on_date_and_time" timestamp(0) 7 | with 8 | time zone NOT NULl, 9 | "intended_date_and_time" timestamp(0) 10 | with 11 | time zone NULL, 12 | "complete_by_date_and_time" timestamp(0) 13 | with 14 | time zone NULL, 15 | "predicted_duration" interval NULL 16 | ); 17 | -------------------------------------------------------------------------------- /.github/workflows/web_server_build.yml: -------------------------------------------------------------------------------- 1 | name: Web Server Build 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: setup 20 | run: sudo apt-get update && sudo apt-get install libudev-dev 21 | - name: Build 22 | run: cargo build --verbose 23 | working-directory: loremaster-web-server 24 | #- name: Run tests 25 | # run: cargo test --verbose 26 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/authentication/remove_stale_web_authentication_login_by_user_name.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{query, PgPool}; 3 | 4 | const QUERY: &str = " 5 | DELETE 6 | FROM 7 | public.web_authentication_login 8 | WHERE 9 | web_authentication_login.user_name = $1 10 | ;"; 11 | 12 | pub async fn remove_stale_web_authentication_login_by_user_name_query( 13 | database_connection: &PgPool, 14 | user_name: &str, 15 | ) -> Result<()> { 16 | query(QUERY) 17 | .bind(user_name) 18 | .execute(database_connection) 19 | .await?; 20 | 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/action/get_all_actions.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | 5 | use crate::data::entity::action::Action; 6 | 7 | const QUERY: &str = " 8 | SELECT 9 | id 10 | , name 11 | FROM 12 | public.action 13 | ;"; 14 | 15 | pub async fn get_all_actions_query(database_connection: &PgPool) -> Result> { 16 | info!("QUERY CALL: get_action_query"); 17 | 18 | let query_result: Vec = query_as::<_, Action>(QUERY) 19 | .fetch_all(database_connection) 20 | .await?; 21 | 22 | Ok(query_result) 23 | } 24 | -------------------------------------------------------------------------------- /documentation/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## Who 4 | 5 | Anyone interested in learning more about this application. 6 | 7 | ## What 8 | 9 | This documentation aims to have all pertinent information about the loremaster application. 10 | Should include things like: 11 | 12 | - Designs for data, user interfaces, functionality implementations 13 | - User stories 14 | 15 | ## Where 16 | 17 | Anywhere. Or nowhere in particular. 18 | 19 | ## When 20 | 21 | Whenever. 22 | 23 | ## Why 24 | 25 | You want to understand the loremaster application better. 26 | You want to contribute to the application or the documentation. 27 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/data/entity/action.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | use crate::{components::combobox::ComboBoxOption, templates::design_system::ComboBoxDatum}; 5 | 6 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 7 | pub struct Action { 8 | pub id: Uuid, 9 | pub name: String, 10 | } 11 | 12 | impl ComboBoxDatum for Action { 13 | fn to_combobox_option(self) -> ComboBoxOption { 14 | ComboBoxOption { 15 | id: self.id, 16 | display_text: self.name, 17 | description: String::new(), 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /loremaster-web-interface/README.md: -------------------------------------------------------------------------------- 1 | # loremaster - Frontend 2 | 3 | ## Setup 4 | 5 | The frontend style libraries have been excluded from version control. 6 | You must download them and place them in the styles/library folder. 7 | 8 | Current style libraries: 9 | 10 | - [Bootstrap 5.1](https://getbootstrap.com/docs/5.1/getting-started/download/) 11 | 12 | Icons 13 | 14 | - [feather icons](https://feathericons.com/) 15 | 16 | ## Perseus CLI 17 | 18 | Statically serve 19 | 20 | ```bash 21 | perseus export -w // export and watch 22 | perseus export -sw 23 | ``` 24 | 25 | Use server 26 | 27 | ```bash 28 | perseus serve -w 29 | ``` 30 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/passkey.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 5 | pub struct Passkey { 6 | pub id: PasskeyId, 7 | pub credential_id: Vec, 8 | pub cose_algorithm: i32, 9 | } 10 | 11 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::Decode, sqlx::Encode)] 12 | pub struct PasskeyId(pub Uuid); 13 | 14 | impl sqlx::Type for PasskeyId { 15 | fn type_info() -> ::TypeInfo { 16 | >::type_info() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /database/configuration/update database roles.sql: -------------------------------------------------------------------------------- 1 | ALTER ROLE developer CREATEDB CREATEROLE NOSUPERUSER; 2 | GRANT developer TO naes; 3 | GRANT application_broker TO loremaster_broker; 4 | --ALTER ROLE '' WITH PASSWORD NULL; 5 | GRANT 6 | SELECT, 7 | UPDATE, 8 | INSERT, 9 | DELETE, 10 | TRUNCATE, 11 | REFERENCES, 12 | CREATE, 13 | CONNECT, 14 | EXECUTE, 15 | USAGE, 16 | TRIGGER, 17 | TEMPORARY ON ALL TABLES IN SCHEMA "public" TO developer; 18 | GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA "public" TO developer; 19 | GRANT 20 | SELECT, 21 | INSERT, 22 | UPDATE, 23 | DELETE ON ALL TABLES IN SCHEMA "public" TO application_broker; -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/popover.rs: -------------------------------------------------------------------------------- 1 | use sycamore::prelude::*; 2 | 3 | #[derive(Prop)] 4 | pub struct PopoverProperties<'popover, G: Html> { 5 | pub classes: &'popover ReadSignal, 6 | pub children: Children<'popover, G>, 7 | } 8 | 9 | #[component] 10 | pub fn Popover<'popover, G: Html>( 11 | context: Scope<'popover>, 12 | PopoverProperties { children, classes }: PopoverProperties<'popover, G>, 13 | ) -> View { 14 | let children = children.call(context); 15 | view! {context, 16 | div(class=(format!("{} popover", classes))) { 17 | (children) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/information.rs: -------------------------------------------------------------------------------- 1 | use sycamore::prelude::*; 2 | 3 | #[derive(Prop)] 4 | pub struct InformationProperties<'info, G: Html> { 5 | pub classes: &'info ReadSignal, 6 | pub children: Children<'info, G>, 7 | } 8 | 9 | #[component] 10 | pub fn Information<'info, G: Html>( 11 | context: Scope<'info>, 12 | InformationProperties { children, classes }: InformationProperties<'info, G>, 13 | ) -> View { 14 | let children = children.call(context); 15 | view! {context, 16 | div(class=(format!("{} information", classes))) { 17 | (children) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/quick_task.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | use super::person::PersonId; 5 | 6 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 7 | pub struct QuickTask { 8 | pub id: Uuid, 9 | pub title: String, 10 | pub description: Option, 11 | pub completed: bool, 12 | } 13 | 14 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 15 | pub struct PersonQuickTask { 16 | pub quick_task_id: Uuid, 17 | pub person_id: PersonId, 18 | pub title: String, 19 | pub description: Option, 20 | pub completed: bool, 21 | } 22 | -------------------------------------------------------------------------------- /loremaster-web-server/src/utility/scheduler.rs: -------------------------------------------------------------------------------- 1 | // use anyhow::{Result, anyhow}; 2 | // use chrono::{DateTime, Utc}; 3 | // use crate::data::entity::schedule::{Schedule}; 4 | 5 | // pub fn CalculateNextRunDate(schedule: Schedule) -> Result>> { 6 | // // If the start date is in the future, it is the next run date 7 | // if schedule.start_date_time < Utc::now() { 8 | // if let Some(excluded_dates) = schedule.excluded_dates { 9 | // if excluded_dates.contains(&schedule.start_date_time.date()) { return Err(anyhow!(""))} 10 | // } 11 | // return Ok(Some(schedule.start_date_time.clone())); 12 | // } 13 | 14 | // return Ok(Some(Utc::now())); 15 | // } 16 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/sleep_schedule.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use time::Time; 3 | use uuid::Uuid; 4 | 5 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct SleepSchedule { 8 | pub id: SleepScheduleId, 9 | pub start_time: Time, 10 | pub end_time: Time, 11 | } 12 | 13 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::Decode, sqlx::Encode)] 14 | pub struct SleepScheduleId(pub Uuid); 15 | 16 | impl sqlx::Type for SleepScheduleId { 17 | fn type_info() -> ::TypeInfo { 18 | >::type_info() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /documentation/entity/chronicle.md: -------------------------------------------------------------------------------- 1 | # Chronicle 2 | 3 | ## Description 4 | 5 | A daily log or recording of all the things related to an individual. 6 | They are the most basic way of tracking progress of a person's actions and progress towards their intentions. 7 | All active users (people) have their own chronicle for each day. 8 | There cannot be more than one chronicle for a given day. 9 | 10 | ## Attributes 11 | 12 | - Id 13 | - A uniquie indentifier 14 | - Date 15 | - The date the chronicle represents 16 | - Owner (Person) 17 | - Documents 18 | - Actions (taken that date) 19 | 20 | ## Related Entites 21 | 22 | - [Person](./person.md) 23 | - [Docuemnt](./document.md) 24 | - [Action](./action.md) 25 | - [] 26 | -------------------------------------------------------------------------------- /infrastructure/google_cloud_platform/README.md: -------------------------------------------------------------------------------- 1 | # Google Cloud Platform - Infrastructure 2 | 3 | ## Terraform 4 | 5 | ### Commands 6 | 7 | Initialize your terraform directory: 8 | 9 | ```bash 10 | terraform init 11 | ``` 12 | 13 | Format your terraform configurations: 14 | 15 | ```bash 16 | terraform fmt 17 | ``` 18 | 19 | Validate the content of your terraform configurations: 20 | 21 | ```bash 22 | terraform validate 23 | ``` 24 | 25 | Check the expected outcome of your terraform configurations: 26 | 27 | ```bash 28 | terraform plan 29 | ``` 30 | 31 | Execute you terraform configurations: 32 | 33 | ```bash 34 | terraform apply 35 | ``` 36 | 37 | Inspect the current state: 38 | 39 | ```bash 40 | terraform show 41 | ``` 42 | -------------------------------------------------------------------------------- /documentation/features/thinking.md: -------------------------------------------------------------------------------- 1 | # Thinking 2 | 3 | ## Ideas 4 | 5 | - reflection 6 | - intention_record 7 | - a list of intention 8 | - reminders 9 | - using dictionary for all terms in database? 10 | - preventing redundant text in database 11 | - choose a date and calculate all that you will have done by that date 12 | - current date: january 1st ,2023 -> at january 1st, 2024 you will have: 13 | - slept for x hours/days 14 | - coded for x hours/days 15 | - ran a half marathon 16 | - etc. 17 | 18 | - Weekday 19 | - default all enabled 20 | - boolean for which weekdays the action can be recorded 21 | - Frequency 22 | - Hour 23 | - Day 24 | - Week 25 | - Month 26 | - Year 27 | - Date list 28 | - Subsequent 29 | -------------------------------------------------------------------------------- /database/table/email_address.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE 2 | "email_address" ( 3 | "id" uuid NOT NULL PRIMARY KEY, 4 | "display" text NOT NULL, 5 | "local_part" text NOT NULL, 6 | "domain" text NOT NULL, 7 | "validated" boolean NOT NULL DEFAULT '0', 8 | "validation_date" timestamp(0) 9 | with 10 | time zone NULL, 11 | "creation_date" timestamp(0) 12 | with time zone NOT NULL 13 | ); 14 | 15 | CREATE INDEX 16 | "email_address_domain_index" ON "email_address" ("domain"); 17 | 18 | CREATE INDEX 19 | "email_address_display_index" ON "email_address" ("display"); 20 | 21 | CREATE INDEX 22 | "email_address_validated_index" ON "email_address" ("validated"); 23 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/state/message_type.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::components::icon::{ 4 | ALERT_OCTAGON_SVG_HTML, ALERT_TRIANGLE_SVG_HTML, CHECK_SVG_HTML, INFO_SVG_HTML, 5 | }; 6 | 7 | #[derive(Deserialize, Serialize, Debug, Clone)] 8 | pub enum MessageType { 9 | Information, 10 | Warning, 11 | Error, 12 | Success, 13 | } 14 | 15 | pub fn get_message_type_icon(message_type: &MessageType) -> &'static str { 16 | match message_type { 17 | MessageType::Information => INFO_SVG_HTML, 18 | MessageType::Warning => ALERT_TRIANGLE_SVG_HTML, 19 | MessageType::Error => ALERT_OCTAGON_SVG_HTML, 20 | MessageType::Success => CHECK_SVG_HTML, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/action/get_action_by_name.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | 5 | use crate::data::entity::action::Action; 6 | 7 | const QUERY: &str = " 8 | SELECT 9 | id 10 | , name 11 | FROM 12 | public.action 13 | WHERE 14 | name = $1 15 | ;"; 16 | 17 | pub async fn get_action_by_name_query( 18 | database_connection: &PgPool, 19 | action: &String, 20 | ) -> Result> { 21 | info!("QUERY CALL: get_action_by_name_query"); 22 | 23 | let query_result: Option = query_as::<_, Action>(QUERY) 24 | .bind(action) 25 | .fetch_optional(database_connection) 26 | .await?; 27 | 28 | Ok(query_result) 29 | } 30 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/authentication/remove_stale_web_authentication_challenges_by_user_name.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query, PgPool}; 4 | 5 | const QUERY: &str = " 6 | DELETE 7 | FROM 8 | public.web_authentication_register 9 | WHERE 10 | web_authentication_register.user_name = $1 11 | ;"; 12 | 13 | pub async fn remove_stale_web_authentication_registers_by_user_name_query( 14 | database_connection: &PgPool, 15 | user_name: &str, 16 | ) -> Result<()> { 17 | info!("QUERY CALL: remove_stale_web_authentication_registers_by_user_name_query"); 18 | query(QUERY) 19 | .bind(user_name) 20 | .execute(database_connection) 21 | .await?; 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/goal/get_goal_by_name.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | 5 | use crate::data::entity::goal::Goal; 6 | 7 | const QUERY: &str = " 8 | SELECT 9 | id 10 | , name 11 | FROM 12 | public.goal 13 | WHERE 14 | name = $1 15 | ;"; 16 | 17 | pub async fn get_goal_by_name_query( 18 | database_connection: &PgPool, 19 | goal: &String, 20 | ) -> Result> { 21 | info!("QUERY CALL: get_goal_by_name_query"); 22 | 23 | let query_result: Option = query_as::<_, Goal>(QUERY) 24 | .bind(goal) 25 | .fetch_optional(database_connection) 26 | .await?; 27 | 28 | Ok(query_result) 29 | } 30 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/web_authentication_key.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | use uuid::Uuid; 4 | 5 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 6 | pub struct WebAuthenticationKey { 7 | pub id: WebAuthenticationKeyId, 8 | pub credential_id: Vec, 9 | pub cose_algorithm: i32, 10 | pub passkey: Value, 11 | } 12 | 13 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::Decode, sqlx::Encode)] 14 | pub struct WebAuthenticationKeyId(pub Uuid); 15 | 16 | impl sqlx::Type for WebAuthenticationKeyId { 17 | fn type_info() -> ::TypeInfo { 18 | >::type_info() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust Rocket File 2 | Rocket.toml 3 | 4 | # Application Configuration File 5 | Loremaster.ron 6 | loremaster.ron 7 | loremaster.toml 8 | Loremaster.toml 9 | 10 | # Rust 11 | target/ 12 | /loremaster-web-server/target 13 | /loremaster-web-interface/target 14 | /loremaster-web-interface/dist 15 | /loremaster-web-interface/pkg 16 | 17 | # Environment files 18 | .env 19 | 20 | # Third Party CSS Frameworks/Stylesheets 21 | /loremaster-web-interface/static/bootstrap-5.2.0-dist/ 22 | /loremaster-web-interface/resources/styles/library/* 23 | 24 | # Terraform 25 | *.gcp-service-account.json 26 | .terraform/ 27 | *.tfstate 28 | application/database/.project 29 | 30 | .DS_Store 31 | loremaster-web-server/.DS_Store 32 | /loremaster-web-interface/.DS_Store 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "bindgen", 4 | "chrono", 5 | "Combobox", 6 | "cose", 7 | "ctap", 8 | "fidokey", 9 | "gloo", 10 | "Hasher", 11 | "listbox", 12 | "openapi", 13 | "reqwasm", 14 | "Rustls", 15 | "webauthn", 16 | "YUBIKEY" 17 | ], 18 | "editor.insertSpaces": false, 19 | "editor.tabSize": 2, 20 | "git.enableCommitSigning": true, 21 | "yaml.schemas": { 22 | "openapi:v3": "file:///home/naes/github/loremaster/documentation/openapi/chronicle/openapi%3A%20%273.0.2%27.yml" 23 | }, 24 | "rust-analyzer.linkedProjects": [ 25 | "./loremaster-web-interface/Cargo.toml", 26 | "./loremaster-web-server/Cargo.toml" 27 | ] 28 | } -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/password/add_password.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query, PgPool}; 4 | use uuid::Uuid; 5 | 6 | const QUERY: &str = " 7 | INSERT INTO 8 | public.password ( 9 | id 10 | , encrypted_password 11 | ) 12 | VALUES 13 | ($1, $2) 14 | ;"; 15 | 16 | pub async fn add_password_query( 17 | database_connection: &PgPool, 18 | encrypted_password: &String, 19 | ) -> Result { 20 | info!("QUERY CALL: add_password_query"); 21 | let new_password_id: Uuid = Uuid::new_v4(); 22 | 23 | query(QUERY) 24 | .bind(new_password_id) 25 | .bind(encrypted_password) 26 | .execute(database_connection) 27 | .await?; 28 | 29 | Ok(new_password_id) 30 | } 31 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/add_password.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query, PgPool}; 4 | use uuid::Uuid; 5 | 6 | use crate::data::entity::person::PersonId; 7 | 8 | const QUERY: &str = " 9 | INSERT INTO 10 | public.person_password ( 11 | person_id 12 | , password_id 13 | ) 14 | VALUES 15 | ($1, $2) 16 | ;"; 17 | 18 | pub async fn add_password_query( 19 | database_connection: &PgPool, 20 | person_id: &PersonId, 21 | password_id: &Uuid, 22 | ) -> Result<()> { 23 | info!("QUERY CALL: add_password_query"); 24 | 25 | query(QUERY) 26 | .bind(person_id.0) 27 | .bind(password_id) 28 | .execute(database_connection) 29 | .await?; 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /documentation/features/challenges.md: -------------------------------------------------------------------------------- 1 | # Challenges 2 | 3 | A feature that gives users something to work towards in various aspects of their life. 4 | 5 | The feature comes with a large set of curated challenges, but also allows users to create their own. 6 | 7 | ## Examples 8 | 9 | - Health 10 | - Diet 11 | - Eat a vegetable 12 | - Eat a vegetable 7 days in a row 13 | - "Plant lover": Only eat plant derived foods/drinks for 30 days in a row. 14 | - Fitness 15 | - Cardio 16 | - Run a mile 17 | - Run a 5k 18 | - Run a half-marathon 19 | - Run a marathon 20 | - Strength 21 | - Do a push up 22 | - Do 10 push ups 23 | - Do 25 push ups 24 | - Do 50 push ups 25 | - Do a pull up 26 | - Cognition 27 | - Read a book 28 | - Read 100 books 29 | -------------------------------------------------------------------------------- /loremaster-database/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "loremaster-database" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | # Error handling 10 | anyhow = "1.0.79" 11 | # Provides async programming foundational functionality 12 | futures = "0.3.30" 13 | loremaster = { path = "../loremaster" } 14 | # Database client/pool/toolkit 15 | sqlx = { version = "0.6.3", features = [ 16 | "json", 17 | "macros", 18 | "migrate", 19 | # "offline", 20 | "postgres", 21 | "runtime-tokio-native-tls", 22 | "time", 23 | "tls", 24 | "uuid", 25 | ] } 26 | # Error/exception handling 27 | thiserror = "1.0.56" 28 | # Backend async I/O functionality 29 | tokio = { version = "1.36.0", features = ["full"] } 30 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/alias_by_id.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{query_as, PgPool}; 3 | use uuid::Uuid; 4 | 5 | #[derive(sqlx::FromRow)] 6 | struct PersonAlias { 7 | pub alias: Option, 8 | } 9 | 10 | const QUERY: &str = " 11 | SELECT 12 | person.alias 13 | FROM 14 | public.person 15 | WHERE 16 | person.id = $1 17 | LIMIT 18 | 1 19 | ;"; 20 | 21 | pub async fn alias_by_id_query( 22 | database_connection: &PgPool, 23 | person_id: &Uuid, 24 | ) -> Result> { 25 | let query_result: Option = query_as::<_, PersonAlias>(QUERY) 26 | .bind(person_id) 27 | .fetch_optional(database_connection) 28 | .await?; 29 | match query_result { 30 | Some(person) => Ok(person.alias), 31 | None => Ok(None), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/navigation/tab/tab_panel.rs: -------------------------------------------------------------------------------- 1 | use sycamore::prelude::*; 2 | 3 | pub type TabIndex = u32; 4 | 5 | #[derive(Prop)] 6 | pub struct TabPanelProperties<'tab_panel, G: Html> { 7 | pub active_tab: &'tab_panel Signal, 8 | pub classes: &'tab_panel ReadSignal, 9 | pub children: Children<'tab_panel, G>, 10 | } 11 | 12 | #[component] 13 | pub fn TabPanel<'tab_panel, G: Html>( 14 | context: Scope<'tab_panel>, 15 | properties: TabPanelProperties<'tab_panel, G>, 16 | ) -> View { 17 | provide_context_ref(context, properties.active_tab); 18 | let children: View = properties.children.call(context); 19 | 20 | view! {context, 21 | div(class=(format!("tab-panel {}", properties.classes.get()))) { 22 | (children) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /loremaster-web-server/src/api/response.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | http::StatusCode, 3 | response::{IntoResponse, Response}, 4 | Json, 5 | }; 6 | use log::error; 7 | use serde_json::json; 8 | use thiserror::Error; 9 | 10 | #[derive(Error, Debug)] 11 | pub enum ApiError { 12 | #[error("{source:?}")] 13 | Anyhow { 14 | #[from] 15 | source: anyhow::Error, 16 | }, 17 | //Something went wrong during authentication 18 | // AuthenticationError(String), 19 | } 20 | 21 | impl IntoResponse for ApiError { 22 | fn into_response(self) -> Response { 23 | error!("{self}"); 24 | 25 | let body: Json = Json(json!({ 26 | "error": "Something went wrong on our side. Sorry.", 27 | })); 28 | 29 | (StatusCode::INTERNAL_SERVER_ERROR, body).into_response() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/authentication/get_web_authentication_login.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{query_as, PgPool}; 3 | 4 | use crate::data::entity::web_authentication_challenge::{ 5 | WebAuthenticationChallenge, WebAuthenticationLogin, 6 | }; 7 | 8 | const QUERY: &str = " 9 | SELECT 10 | web_authentication_login.* 11 | FROM 12 | public.web_authentication_login 13 | WHERE 14 | web_authentication_login.user_name = $1 15 | ;"; 16 | 17 | pub async fn get_web_authentication_login_query( 18 | database_connection: &PgPool, 19 | user_name: &str, 20 | ) -> Result> { 21 | let query_result: Option = query_as(QUERY) 22 | .bind(user_name) 23 | .fetch_optional(database_connection) 24 | .await?; 25 | 26 | Ok(query_result) 27 | } 28 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/goal/create_goal.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | 5 | use uuid::Uuid; 6 | 7 | use crate::data::entity::goal::Goal; 8 | 9 | const QUERY: &str = " 10 | INSERT INTO 11 | public.goal ( 12 | id 13 | , name 14 | ) 15 | VALUES 16 | ($1, $2) 17 | RETURNING 18 | id 19 | , name 20 | ;"; 21 | 22 | pub async fn create_goal_query(database_connection: &PgPool, goal: &String) -> Result { 23 | info!("QUERY CALL: create_action_query"); 24 | let new_id: Uuid = Uuid::new_v4(); 25 | 26 | let query_result: Goal = query_as::<_, Goal>(QUERY) 27 | .bind(new_id) 28 | .bind(goal) 29 | .fetch_one(database_connection) 30 | .await?; 31 | 32 | Ok(query_result) 33 | } 34 | -------------------------------------------------------------------------------- /.idea/loremaster.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/meta_by_id.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{query_as, PgPool}; 3 | 4 | use crate::data::entity::person::{PersonId, PersonMeta}; 5 | 6 | const QUERY: &str = " 7 | SELECT 8 | person.id 9 | , email_address.display as email_address 10 | , registration_date 11 | , alias 12 | FROM 13 | public.person 14 | INNER JOIN 15 | public.email_address ON email_address.id = person.email_address_id 16 | WHERE 17 | person.id = $1 18 | LIMIT 19 | 1 20 | ;"; 21 | 22 | pub async fn meta_by_id_query( 23 | database_connection: &PgPool, 24 | person_id: &PersonId, 25 | ) -> Result> { 26 | let query_result: Option = query_as::<_, PersonMeta>(QUERY) 27 | .bind(person_id) 28 | .fetch_optional(database_connection) 29 | .await?; 30 | Ok(query_result) 31 | } 32 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/surreal_handler.rs: -------------------------------------------------------------------------------- 1 | // use anyhow::{anyhow, Result}; 2 | // use surrealdb::{Datastore, Session}; 3 | 4 | // const SURREAL_DATABASE_FILE_PATH: &str = "file://loremaster.db"; 5 | // const SURREAL_NAMESPACE: &str = "loremaster"; 6 | // const SURREAL_DATABASE_NAME: &str = "loremaster"; 7 | 8 | // pub struct SurrealDatabaseHandler { 9 | // pub session: Session, 10 | // pub data_store: Datastore, 11 | // } 12 | 13 | // impl SurrealDatabaseHandler { 14 | // pub async fn new() -> Result { 15 | // Ok(SurrealDatabaseHandler { 16 | // session: Session::for_db(SURREAL_NAMESPACE, SURREAL_DATABASE_NAME), 17 | // data_store: Datastore::new(SURREAL_DATABASE_FILE_PATH) 18 | // .await 19 | // .map_err(|error| anyhow!("{}", error))?, 20 | // }) 21 | // } 22 | // } 23 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/schedule.rs: -------------------------------------------------------------------------------- 1 | // use chrono::{Date, DateTime, Utc, Weekday}; 2 | // use std::collections::HashSet; 3 | // use uuid::Uuid; 4 | 5 | // pub struct Schedule { 6 | // pub id: Uuid, 7 | // pub start_date_time: DateTime, 8 | // pub end_date_time: Option>, 9 | // pub weekday_occurences: HashSet, 10 | // pub date_interval: TimeInterval, 11 | // pub time_interval: TimeInterval, 12 | // pub excluded_dates: Option>>, 13 | // } 14 | 15 | // pub struct TimeInterval { 16 | // pub value: u32, 17 | // pub time_unit: UnitOfTime, 18 | // } 19 | 20 | // pub enum UnitOfTime { 21 | // Second, 22 | // Minute, 23 | // Hour, 24 | // } 25 | 26 | // pub enum UnitOfMonth { 27 | // Day, 28 | // Week, 29 | // Month, 30 | // Year, 31 | // Decade, 32 | // Century 33 | // } 34 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/action/create_action.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | 5 | use uuid::Uuid; 6 | 7 | use crate::data::entity::action::Action; 8 | 9 | const QUERY: &str = " 10 | INSERT INTO 11 | public.action ( 12 | id 13 | , name 14 | ) 15 | VALUES 16 | ($1, $2) 17 | RETURNING 18 | id 19 | , name 20 | ;"; 21 | 22 | pub async fn create_action_query(database_connection: &PgPool, action: &String) -> Result { 23 | info!("QUERY CALL: create_action_query"); 24 | let new_id: Uuid = Uuid::new_v4(); 25 | 26 | let query_result: Action = query_as::<_, Action>(QUERY) 27 | .bind(new_id) 28 | .bind(action) 29 | .fetch_one(database_connection) 30 | .await?; 31 | 32 | Ok(query_result) 33 | } 34 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/email_address.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use sqlx::types::time::OffsetDateTime; 3 | use uuid::Uuid; 4 | 5 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct EmailAddress { 8 | pub id: EmailAddressId, 9 | pub display: String, 10 | pub local_part: String, 11 | pub domain: String, 12 | pub validated: bool, 13 | pub validation_date: Option, 14 | pub creation_date: OffsetDateTime, 15 | } 16 | 17 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::Decode, sqlx::Encode)] 18 | pub struct EmailAddressId(pub Uuid); 19 | 20 | impl sqlx::Type for EmailAddressId { 21 | fn type_info() -> ::TypeInfo { 22 | >::type_info() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/web_authentication_challenge.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | use uuid::Uuid; 4 | 5 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 6 | pub struct WebAuthenticationChallenge { 7 | pub id: WebAuthenticationChallengeId, 8 | pub user_name: String, 9 | pub passkey: Value, 10 | } 11 | 12 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::Decode, sqlx::Encode)] 13 | pub struct WebAuthenticationChallengeId(pub Uuid); 14 | 15 | impl sqlx::Type for WebAuthenticationChallengeId { 16 | fn type_info() -> ::TypeInfo { 17 | >::type_info() 18 | } 19 | } 20 | 21 | pub type WebAuthenticationRegister = WebAuthenticationChallenge; 22 | pub type WebAuthenticationLogin = WebAuthenticationChallenge; 23 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/goal.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | use super::person::PersonId; 5 | 6 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct Goal { 9 | pub id: GoalId, 10 | pub name: String, 11 | } 12 | 13 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::Decode, sqlx::Encode)] 14 | pub struct GoalId(pub Uuid); 15 | 16 | impl sqlx::Type for GoalId { 17 | fn type_info() -> ::TypeInfo { 18 | >::type_info() 19 | } 20 | } 21 | 22 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 23 | #[serde(rename_all = "camelCase")] 24 | pub struct PersonGoal { 25 | pub person_id: PersonId, 26 | pub goal_id: GoalId, 27 | pub goal_name: String, 28 | } 29 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/chronicle/chronicle_by_id.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{query_as, PgPool}; 3 | use uuid::Uuid; 4 | 5 | use crate::data::entity::chronicle::Chronicle; 6 | 7 | const QUERY: &str = " 8 | SELECT 9 | chronicle.id 10 | , chronicle.person_id 11 | , chronicle.date_recorded 12 | , chronicle.notes 13 | , chronicle.creation_time 14 | FROM 15 | public.chronicle 16 | WHERE 17 | chronicle.id = $1 18 | LIMIT 19 | 1 20 | ;"; 21 | 22 | pub async fn chronicle_by_id_query( 23 | database_connection: &PgPool, 24 | chronicle_id: &Uuid, 25 | ) -> Result> { 26 | let query_result: Option = query_as::<_, Chronicle>(QUERY) 27 | .bind(chronicle_id) 28 | .fetch_optional(database_connection) 29 | .await?; 30 | 31 | Ok(query_result) 32 | } 33 | -------------------------------------------------------------------------------- /documentation/entity/action.md: -------------------------------------------------------------------------------- 1 | # Action 2 | 3 | ## Description 4 | 5 | The fact or process of doing something, typically to achieve an intention. 6 | A thing done by a person. 7 | Actions can be repeated. 8 | 9 | ## Attributes 10 | 11 | - Name 12 | - Category 13 | 14 | ## Related Entities 15 | 16 | - [Intention](./intention.md) 17 | - Intentions are realized through actions 18 | - Intentions lead users to plan actions 19 | - [Outcome](./outcome.md) 20 | - Actions results are outcomes 21 | - An action might have multiple outcomes 22 | - [Chronicle](./chronicle.md) 23 | - Actions are temporal. They are completed at some point in time. When recording actions they can be related to chronicles, as chronicles keep track of time. 24 | - [Action Category](./action_category.md) 25 | - There are different types of actions which are categorized. 26 | - [Contingent Action](./contingent_action.md) 27 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/authentication/get_web_authentication_by_user_name.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{query_as, PgPool}; 3 | 4 | use crate::data::entity::web_authentication_challenge::WebAuthenticationChallenge; 5 | 6 | const QUERY: &str = " 7 | SELECT 8 | id 9 | , user_name 10 | , passkey 11 | FROM 12 | public.web_authentication_register 13 | WHERE 14 | web_authentication_register.user_name = $1 15 | LIMIT 16 | 1 17 | ;"; 18 | 19 | pub async fn get_web_authentication_by_user_name_query( 20 | database_connection: &PgPool, 21 | user_name: &str, 22 | ) -> Result> { 23 | let query_result: Option = query_as(QUERY) 24 | .bind(user_name) 25 | .fetch_optional(database_connection) 26 | .await?; 27 | 28 | Ok(query_result) 29 | } 30 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/update_email_address.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{query_as, PgPool}; 3 | 4 | use crate::data::entity::{ 5 | email_address::EmailAddressId, 6 | person::{Person, PersonId}, 7 | }; 8 | 9 | const QUERY: &str = " 10 | UPDATE 11 | person 12 | SET 13 | email_address_id = $2 14 | WHERE 15 | id = $1 16 | RETURNING 17 | id, 18 | email_address_id, 19 | encrypted_password, 20 | registration_date, 21 | alias, 22 | chronicle_id 23 | ;"; 24 | 25 | pub async fn update_email_address_query( 26 | database_connection: &PgPool, 27 | person_id: &PersonId, 28 | email_address_id: &EmailAddressId, 29 | ) -> Result { 30 | let query_result: Person = query_as::<_, Person>(QUERY) 31 | .bind(person_id) 32 | .bind(email_address_id) 33 | .fetch_one(database_connection) 34 | .await?; 35 | Ok(query_result) 36 | } 37 | -------------------------------------------------------------------------------- /database/configuration/create database roles.sql: -------------------------------------------------------------------------------- 1 | BEGIN CREATE ROLE developer WITH NOSUPERUSER CREATEDB CREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS CONNECTION 2 | LIMIT 3 | UNLIMITED; 4 | EXCEPTION 5 | WHEN DUPLICATE_OBJECT THEN RAISE NOTICE 'not creating role developer -- it already exists'; 6 | END; 7 | BEGIN CREATE ROLE application_broker WITH NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION NOBYPASSRLS CONNECTION 8 | LIMIT 9 | UNLIMITED; 10 | EXCEPTION 11 | WHEN DUPLICATE_OBJECT THEN RAISE NOTICE 'not creating role broker -- it already exists'; 12 | END; 13 | BEGIN CREATE ROLE naes WITH NOLOGIN; 14 | EXCEPTION 15 | WHEN DUPLICATE_OBJECT THEN RAISE NOTICE 'not creating role naes -- it already exists'; 16 | END; 17 | BEGIN CREATE ROLE loremaster_broker WITH NOLOGIN; 18 | EXCEPTION 19 | WHEN DUPLICATE_OBJECT THEN RAISE NOTICE 'not creating role loremaster_broker -- it already exists'; 20 | END; -------------------------------------------------------------------------------- /documentation/implementation/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | ## Options 4 | 5 | - Password 6 | - [fido2](https://en.wikipedia.org/wiki/FIDO2_Project) through Client to Authenticator Protocol ([CTAP](https://en.wikipedia.org/wiki/Client_to_Authenticator_Protocol)) 7 | 8 | ## How 9 | We therefore act as the gatekeeper of the user's data, and must also be aware of their key. This means that we are responsible for keeping the user's key secure. 10 | 11 | Flow looks something like this: 12 | 13 | 1. User provides email and desired password in registration. 14 | - A choice must be made at this step, as the user's password is in plain text when typed into the UI. 15 | - Hash the user's password on the UI/Client side before passing to server 16 | - Use SSL to ensure 17 | 2. The password is passed from the client to the backend and the password is immediately hashed 18 | - The password is hashed 19 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/switch.rs: -------------------------------------------------------------------------------- 1 | use sycamore::prelude::*; 2 | use web_sys::Event; 3 | 4 | #[derive(Prop)] 5 | pub struct SwitchProperties<'switch> { 6 | pub toggled: &'switch Signal, 7 | pub classes: &'switch ReadSignal, 8 | } 9 | 10 | #[component] 11 | pub fn Switch<'switch, G: Html>( 12 | context: Scope<'switch>, 13 | SwitchProperties { toggled, classes }: SwitchProperties<'switch>, 14 | ) -> View { 15 | let on_click = move |_: Event| match toggled.get().as_ref() { 16 | true => toggled.set(false), 17 | false => toggled.set(true), 18 | }; 19 | 20 | view! {context, 21 | label(class=classes) { 22 | input( 23 | bind:checked=toggled, 24 | type="checkbox", 25 | id="", 26 | on:click=on_click 27 | ) {} 28 | span() {} 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/intention.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use time::{Duration, OffsetDateTime}; 3 | use uuid::Uuid; 4 | 5 | use super::{chronicle::ChronicleId, person::PersonId}; 6 | 7 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct Intention { 10 | pub id: IntentionId, 11 | pub action_id: Uuid, 12 | pub person_id: PersonId, 13 | pub chronicle_id: Option, 14 | pub intended_time: Option, 15 | pub expected_duration: Option, 16 | } 17 | 18 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::Decode, sqlx::Encode)] 19 | pub struct IntentionId(pub Uuid); 20 | 21 | impl sqlx::Type for IntentionId { 22 | fn type_info() -> ::TypeInfo { 23 | >::type_info() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/goal_is_related.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query, PgPool}; 4 | 5 | use crate::data::entity::{goal::GoalId, person::PersonId}; 6 | 7 | const QUERY: &str = " 8 | SELECT 9 | person_goal.action_id 10 | FROM 11 | person_goal 12 | WHERE 13 | person_goal.person_id = $1 14 | AND person_goal.goal_id = $2 15 | LIMIT 16 | 1 17 | ;"; 18 | 19 | pub async fn goal_is_related_query( 20 | database_connection: &PgPool, 21 | person_id: &PersonId, 22 | goal_id: &GoalId, 23 | ) -> Result { 24 | info!("QUERY CALL: goal_is_related_query"); 25 | 26 | let query_result = query(QUERY) 27 | .bind(person_id) 28 | .bind(goal_id) 29 | .fetch_optional(database_connection) 30 | .await?; 31 | 32 | match query_result { 33 | Some(_relation) => Ok(true), 34 | None => Ok(false), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/chronicle/chronicle_by_date.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{query_as, PgPool}; 3 | use time::OffsetDateTime; 4 | use uuid::Uuid; 5 | 6 | use crate::data::entity::chronicle::Chronicle; 7 | 8 | const QUERY: &str = " 9 | SELECT 10 | chronicle.id 11 | , chronicle.person_id 12 | , chronicle.date_recorded 13 | FROM 14 | public.chronicle 15 | WHERE 16 | chronicle.date_recorded = $1 17 | AND chronicle.person_id = $2 18 | LIMIT 1 19 | ;"; 20 | 21 | pub async fn chronicle_by_date_query( 22 | database_connection: &PgPool, 23 | chronicle_date: &OffsetDateTime, 24 | person_id: &Uuid, 25 | ) -> Result> { 26 | let query_result = query_as::<_, Chronicle>(QUERY) 27 | .bind(chronicle_date) 28 | .bind(person_id) 29 | .fetch_optional(database_connection) 30 | .await?; 31 | 32 | Ok(query_result) 33 | } 34 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/add_web_authentication_key.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query, PgPool}; 4 | 5 | use crate::data::entity::{person::PersonId, web_authentication_key::WebAuthenticationKeyId}; 6 | 7 | const QUERY: &str = " 8 | INSERT INTO 9 | public.person_web_authentication_key ( 10 | person_id 11 | , web_authentication_key_id 12 | ) 13 | VALUES 14 | ($1, $2) 15 | ;"; 16 | 17 | pub async fn add_web_authentication_key_query( 18 | database_connection: &PgPool, 19 | person_id: &PersonId, 20 | web_authentication_key_id: &WebAuthenticationKeyId, 21 | ) -> Result<()> { 22 | info!("QUERY CALL: add_web_authentication_key_query"); 23 | 24 | query(QUERY) 25 | .bind(person_id) 26 | .bind(web_authentication_key_id) 27 | .execute(database_connection) 28 | .await?; 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /loremaster-web-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lukemathwalker/cargo-chef:latest as chef 2 | WORKDIR /app 3 | 4 | FROM chef AS planner 5 | COPY . . 6 | RUN cargo chef prepare --recipe-path recipe.json 7 | 8 | FROM chef AS builder 9 | COPY --from=planner /app/recipe.json recipe.json 10 | # Build dependencies - this is the caching Docker layer! 11 | RUN cargo chef cook --release --recipe-path recipe.json 12 | # Build application 13 | COPY . . 14 | RUN cargo build --release --bin loremaster 15 | 16 | # We do not need the Rust toolchain to run the binary! 17 | FROM ubuntu:20.04 AS runtime 18 | WORKDIR /app 19 | COPY --from=builder /app/target/release/loremaster /usr/local/bin 20 | COPY Loremaster.ron /usr/local/bin 21 | COPY certs/ /usr/local/bin/certs 22 | COPY frontend/dist/exported/ /usr/local/bin/frontend/dist/exported 23 | RUN apt-get -y update 24 | RUN apt-get -y install openssl 25 | #RUN apt-get -y install libssl-dev 26 | ENTRYPOINT ["/usr/local/bin/loremaster"] -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/add_goal.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use log::info; 3 | use sqlx::{query, PgPool}; 4 | 5 | use crate::data::entity::{goal::GoalId, person::PersonId}; 6 | 7 | const QUERY: &str = " 8 | INSERT INTO 9 | public.person_goal ( 10 | person_id 11 | , goal_id 12 | ) 13 | VALUES 14 | ($1, $2) 15 | ;"; 16 | 17 | pub async fn add_goal_query( 18 | database_connection: &PgPool, 19 | person_id: &PersonId, 20 | goal_id: &GoalId, 21 | ) -> Result<()> { 22 | info!("QUERY CALL: add_goal_query"); 23 | 24 | let updated_row_count: u64 = query(QUERY) 25 | .bind(person_id) 26 | .bind(goal_id) 27 | .execute(database_connection) 28 | .await? 29 | .rows_affected(); 30 | 31 | if updated_row_count < 1_u64 { 32 | return Err(anyhow!("No rows were updated! query: add_goal_query")); 33 | } 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/authentication/get_web_authentication_register.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_scalar, PgPool}; 4 | 5 | use crate::data::entity::web_authentication_challenge::WebAuthenticationChallengeId; 6 | 7 | const QUERY: &str = " 8 | SELECT 9 | id 10 | FROM 11 | public.web_authentication_register 12 | WHERE 13 | web_authentication_register.user_name = $1 14 | ;"; 15 | 16 | pub async fn get_optional_web_authentication_id_by_user_name_query( 17 | database_connection: &PgPool, 18 | user_name: &str, 19 | ) -> Result> { 20 | info!("QUERY CALL: get_optional_web_authentication_id_by_user_name_query"); 21 | let query_result: Option = query_scalar(QUERY) 22 | .bind(user_name) 23 | .fetch_optional(database_connection) 24 | .await?; 25 | 26 | Ok(query_result) 27 | } 28 | -------------------------------------------------------------------------------- /loremaster-web-server/src/configuration/logging.rs: -------------------------------------------------------------------------------- 1 | use env_logger::{Builder, Target}; 2 | use log::LevelFilter; 3 | use std::io::Write; 4 | use time::{format_description::well_known::Rfc3339, OffsetDateTime}; 5 | 6 | use super::application::LoremasterWebServerConfiguration; 7 | 8 | pub fn configure_logging(configuration: &LoremasterWebServerConfiguration) { 9 | let environment: String = configuration.environment.to_string().to_ascii_uppercase(); 10 | Builder::new() 11 | .target(Target::Stdout) 12 | .format(move |buf, record| -> Result<(), std::io::Error> { 13 | writeln!( 14 | buf, 15 | "[LOREMASTER_{}]: [{}] [{}] - {}", 16 | environment, 17 | OffsetDateTime::now_utc().format(&Rfc3339).unwrap(), 18 | record.level(), 19 | record.args() 20 | ) 21 | }) 22 | .filter(None, LevelFilter::Info) 23 | .init(); 24 | } 25 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/notification/alert.rs: -------------------------------------------------------------------------------- 1 | use sycamore::prelude::*; 2 | use time::Duration; 3 | 4 | #[derive(Prop)] 5 | pub struct AlertProperties<'a> { 6 | pub message_title: &'a ReadSignal, 7 | pub message_body: &'a ReadSignal, 8 | pub display_time: &'a ReadSignal>, 9 | } 10 | 11 | #[component] 12 | pub fn Alert<'a, 'b: 'a, G: Html>( 13 | context: Scope<'a>, 14 | AlertProperties { 15 | message_title, 16 | message_body, 17 | display_time, 18 | }: AlertProperties<'b>, 19 | ) -> View { 20 | view! { context, 21 | div(class="notification-toast-alert", id="") { 22 | image(class="fi-check-circle") {} 23 | div() { 24 | h5(class="") { (message_title.get()) } 25 | p(class="") { (message_body.get()) } 26 | } 27 | button() { image(class="fi-x") {} } 28 | 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/action_is_related.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{postgres::PgRow, query, PgPool}; 4 | 5 | use crate::data::entity::{action::ActionId, person::PersonId}; 6 | 7 | const QUERY: &str = " 8 | SELECT 9 | person_action.action_id 10 | FROM 11 | person_action 12 | WHERE 13 | person_action.person_id = $1 14 | AND person_action.action_id = $2 15 | LIMIT 16 | 1 17 | ;"; 18 | 19 | pub async fn action_is_related_query( 20 | database_connection: &PgPool, 21 | person_id: &PersonId, 22 | action_id: &ActionId, 23 | ) -> Result { 24 | info!("QUERY CALL: action_is_related_query"); 25 | 26 | let query_result: Option = query(QUERY) 27 | .bind(person_id) 28 | .bind(action_id) 29 | .fetch_optional(database_connection) 30 | .await?; 31 | 32 | match query_result { 33 | Some(_relation) => Ok(true), 34 | None => Ok(false), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/remove_one_goal.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use log::info; 3 | use sqlx::{query, PgPool}; 4 | 5 | use crate::data::entity::{goal::GoalId, person::PersonId}; 6 | 7 | const QUERY: &str = " 8 | DELETE FROM 9 | public.person_goal 10 | WHERE 11 | person_goal.person_id = $1 12 | AND person_goal.goal_id = $2 13 | ;"; 14 | 15 | pub async fn remove_one_goal_query( 16 | database_connection: &PgPool, 17 | person_id: &PersonId, 18 | goal_id: &GoalId, 19 | ) -> Result<()> { 20 | info!("QUERY CALL: remove_one_goal_query"); 21 | 22 | let updated_row_count: u64 = query(QUERY) 23 | .bind(person_id) 24 | .bind(goal_id) 25 | .execute(database_connection) 26 | .await? 27 | .rows_affected(); 28 | 29 | if updated_row_count < 1_u64 { 30 | return Err(anyhow!( 31 | "No rows were updated! query: remove_one_goal_query" 32 | )); 33 | } 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/add_action.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use log::info; 3 | use sqlx::{query, PgPool}; 4 | 5 | use crate::data::entity::{action::ActionId, person::PersonId}; 6 | 7 | const QUERY: &str = " 8 | INSERT INTO 9 | public.person_action ( 10 | person_id 11 | , action_id 12 | ) 13 | VALUES 14 | ($1, $2) 15 | ;"; 16 | 17 | pub async fn add_action_query( 18 | database_connection: &PgPool, 19 | person_id: &PersonId, 20 | action_id: &ActionId, 21 | ) -> Result<()> { 22 | info!("QUERY CALL: add_action_query"); 23 | 24 | let updated_row_count: u64 = query(QUERY) 25 | .bind(person_id) 26 | .bind(action_id) 27 | .execute(database_connection) 28 | .await? 29 | .rows_affected(); 30 | 31 | if updated_row_count < 1_u64 { 32 | return Err(anyhow!( 33 | "No rows were updated! query: update_person_sleep_schedule_query" 34 | )); 35 | } 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/theme_toggle.rs: -------------------------------------------------------------------------------- 1 | use sycamore::prelude::*; 2 | use web_sys::{window, Event}; 3 | 4 | use crate::components::icon::{MOON_SVG_HTML, SUN_SVG_HTML}; 5 | 6 | #[component] 7 | pub fn ThemeToggle(context: Scope) -> View { 8 | let theme_switch_handler = move |event: Event| { 9 | event.prevent_default(); 10 | let root = window() 11 | .unwrap() 12 | .document() 13 | .unwrap() 14 | .query_selector(":root") 15 | .unwrap(); 16 | match root { 17 | Some(element) => element.class_list().toggle("light").unwrap(), 18 | None => false, 19 | }; 20 | }; 21 | view! {context, 22 | button( 23 | on:click=theme_switch_handler, 24 | title="Switch color theme" 25 | ) { 26 | span(dangerously_set_inner_html=MOON_SVG_HTML) {} 27 | span(dangerously_set_inner_html=SUN_SVG_HTML) {} 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/authentication/add_web_authentication_key.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query, PgPool}; 4 | 5 | use crate::data::entity::web_authentication_key::WebAuthenticationKey; 6 | 7 | const QUERY: &str = " 8 | INSERT INTO 9 | public.web_authentication_key ( 10 | id 11 | , credential_id 12 | , cose_algorithm 13 | , passkey 14 | ) 15 | VALUES 16 | ($1, $2, $3, $4) 17 | ;"; 18 | 19 | pub async fn add_web_authentication_key_query( 20 | database_connection: &PgPool, 21 | web_authentication_key: &WebAuthenticationKey, 22 | ) -> Result<()> { 23 | info!("QUERY CALL: add_web_authentication_key_query"); 24 | 25 | query(QUERY) 26 | .bind(&web_authentication_key.id) 27 | .bind(&web_authentication_key.credential_id) 28 | .bind(web_authentication_key.cose_algorithm) 29 | .bind(&web_authentication_key.passkey) 30 | .execute(database_connection) 31 | .await?; 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/sleep_schedule/get_sleep_schedule_by_time.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | use time::Time; 5 | 6 | use crate::data::entity::sleep_schedule::SleepSchedule; 7 | 8 | const QUERY: &str = " 9 | SELECT 10 | sleep_schedule.id 11 | ,sleep_schedule.start_time 12 | ,sleep_schedule.end_time 13 | FROM 14 | public.sleep_schedule 15 | WHERE 16 | sleep_schedule.start_time = $1 17 | AND sleep_schedule.end_time = $2 18 | LIMIT 19 | 1 20 | ;"; 21 | 22 | pub async fn get_sleep_schedule_by_time_query( 23 | database_connection: &PgPool, 24 | start_time: &Time, 25 | end_time: &Time, 26 | ) -> Result> { 27 | info!("QUERY CALL: get_sleep_schedule_by_time_query"); 28 | let query_result: Option = query_as::<_, SleepSchedule>(QUERY) 29 | .bind(start_time) 30 | .bind(end_time) 31 | .fetch_optional(database_connection) 32 | .await?; 33 | Ok(query_result) 34 | } 35 | -------------------------------------------------------------------------------- /loremaster-web-server/requests.http: -------------------------------------------------------------------------------- 1 | @test_password=Test123! 2 | 3 | GET http://localhost:8000/ 4 | 5 | ### REGISTER 6 | POST http://localhost:8000/authentication/register HTTP/1.1 7 | Content-Type: application/x-www-form-urlencoded 8 | 9 | email_address=person@loremaster.xyz 10 | &password={{test_password}} 11 | 12 | ### LOGIN 13 | POST http://localhost:8000/authentication/authenticate HTTP/1.1 14 | Content-Type: application/x-www-form-urlencoded 15 | 16 | email_address=person@loremaster.xyz 17 | &password={{test_password}} 18 | 19 | ### LOGOUT 20 | POST http://localhost:8000/authentication/logout HTTP/1.1 21 | 22 | ### 23 | GET http://localhost:8000/chronicle/today 24 | 25 | ### 26 | GET http://localhost:8000/chronicle/today 27 | 28 | ### 29 | GET http://localhost:8000/chronicle/by_date 30 | 31 | ### 32 | GET http://localhost:8000/chronicle/by_id 33 | 34 | ### 35 | GET http://localhost:8000/chronicle/ 36 | 37 | ### 38 | GET http://localhost:8000/chronicle/server_time 39 | 40 | ### 41 | GET http://localhost:8000/chronicle/example -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/get_person_sleep_schedule.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | 5 | use crate::data::entity::{person::PersonId, sleep_schedule::SleepSchedule}; 6 | 7 | const QUERY: &str = " 8 | SELECT 9 | sleep_schedule.id 10 | , sleep_schedule.start_time 11 | , sleep_schedule.end_time 12 | FROM 13 | public.sleep_schedule 14 | INNER JOIN 15 | public.person_sleep_schedule ON sleep_schedule.id = person_sleep_schedule.sleep_schedule_id 16 | WHERE 17 | person_sleep_schedule.person_id = $1 18 | LIMIT 19 | 1 20 | ;"; 21 | 22 | pub async fn get_person_sleep_schedule_query( 23 | database_connection: &PgPool, 24 | person_id: &PersonId, 25 | ) -> Result> { 26 | info!("QUERY CALL: get_person_sleep_schedule_query"); 27 | let query_result: Option = query_as::<_, SleepSchedule>(QUERY) 28 | .bind(person_id) 29 | .fetch_optional(database_connection) 30 | .await?; 31 | Ok(query_result) 32 | } 33 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/chronicle/current_chronicle_by_person.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{query_as, PgPool}; 3 | use time::OffsetDateTime; 4 | use uuid::Uuid; 5 | 6 | use crate::data::entity::chronicle::Chronicle; 7 | 8 | const QUERY: &str = " 9 | SELECT DISTINCT 10 | chronicle.id 11 | , chronicle.person_id 12 | , chronicle.date_recorded 13 | , chronicle.notes 14 | , chronicle.creation_time 15 | FROM 16 | public.chronicle 17 | WHERE 18 | chronicle.date_recorded = $1 19 | AND chronicle.person_id = $2 20 | LIMIT 1 21 | ;"; 22 | 23 | pub async fn get_current_chronicle_by_person_query( 24 | database_connection: &PgPool, 25 | date: &OffsetDateTime, 26 | person_id: &Uuid, 27 | ) -> Result> { 28 | let query_result: Option = query_as::<_, Chronicle>(QUERY) 29 | .bind(date.date()) 30 | .bind(person_id) 31 | .fetch_optional(database_connection) 32 | .await?; 33 | 34 | Ok(query_result) 35 | } 36 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/email_address/email_address_in_use.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use email_address::EmailAddress; 3 | use log::info; 4 | use sqlx::{query_scalar, PgPool}; 5 | use uuid::Uuid; 6 | 7 | const QUERY: &str = " 8 | SELECT 9 | person.id 10 | , email_address.display as email_address 11 | FROM 12 | public.email_address 13 | INNER JOIN 14 | public.person ON public.email_address.id = person.email_address_id 15 | WHERE 16 | email_address.local_part = $1 17 | AND email_address.domain = $2 18 | LIMIT 19 | 1 20 | ;"; 21 | 22 | pub async fn email_address_in_use_query( 23 | database_connection: &PgPool, 24 | email_address: &EmailAddress, 25 | ) -> Result { 26 | info!("QUERY CALL: email_address_in_use_query"); 27 | let query_result: Option = query_scalar(QUERY) 28 | .bind(email_address.local_part()) 29 | .bind(email_address.domain()) 30 | .fetch_optional(database_connection) 31 | .await?; 32 | 33 | Ok(query_result.is_some()) 34 | } 35 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/goal/get_goal_list.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | 5 | use crate::data::entity::{goal::Goal, person::PersonId}; 6 | 7 | const QUERY: &str = " 8 | SELECT 9 | id 10 | , name 11 | FROM 12 | public.goal 13 | ;"; 14 | 15 | const PERSON_QUERY: &str = " 16 | SELECT 17 | goal.id 18 | , goal.name 19 | FROM 20 | public.goal 21 | INNER JOIN 22 | public.person_goal ON goal.id = person_goal.goal_id 23 | WHERE 24 | person_goal.person_id = $1 25 | "; 26 | 27 | pub async fn get_goal_list_query( 28 | database_connection: &PgPool, 29 | person_id: Option<&PersonId>, 30 | ) -> Result> { 31 | info!("QUERY CALL: get_goal_list_query"); 32 | 33 | match person_id { 34 | Some(id) => Ok(query_as::<_, Goal>(PERSON_QUERY) 35 | .bind(id) 36 | .fetch_all(database_connection) 37 | .await?), 38 | None => Ok(query_as::<_, Goal>(QUERY) 39 | .fetch_all(database_connection) 40 | .await?), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/templates/about.rs: -------------------------------------------------------------------------------- 1 | use perseus::{engine_only_fn, template::Template}; 2 | use sycamore::{ 3 | prelude::{view, Html, SsrNode, View}, 4 | reactive::{BoundedScope, Scope}, 5 | }; 6 | 7 | use crate::components::container::Container; 8 | 9 | const PAGE_TITLE: &str = "About | Loremaster"; 10 | const PAGE_ROUTE_PATH: &str = "about"; 11 | 12 | pub fn about_page<'page, G: Html>(context: BoundedScope<'_, 'page>) -> View { 13 | view! {context, 14 | Container(title="About") { 15 | div(class="d-flex flex-column flex-grow-1 p-4 align-items-center") { 16 | h1(class="display-3") { "About" } 17 | p() { "This is a website." } 18 | } 19 | } 20 | } 21 | } 22 | 23 | #[engine_only_fn] 24 | pub fn head(context: Scope) -> View { 25 | view! {context, 26 | title { (PAGE_TITLE) } 27 | } 28 | } 29 | 30 | pub fn get_template() -> Template { 31 | Template::build(PAGE_ROUTE_PATH) 32 | .view(about_page) 33 | .head(head) 34 | .build() 35 | } 36 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/authentication/add_web_authentication_login.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{query_as, PgPool}; 3 | 4 | use crate::data::entity::web_authentication_challenge::WebAuthenticationLogin; 5 | 6 | const QUERY: &str = " 7 | INSERT INTO 8 | public.web_authentication_login ( 9 | id 10 | , user_name 11 | , passkey 12 | ) 13 | VALUES 14 | ($1, $2, $3) 15 | RETURNING 16 | id 17 | , user_name 18 | , passkey 19 | ;"; 20 | 21 | pub async fn add_web_authentication_login_query( 22 | database_connection: &PgPool, 23 | web_authentication_challenge: &WebAuthenticationLogin, 24 | ) -> Result { 25 | let query_result: WebAuthenticationLogin = query_as::<_, WebAuthenticationLogin>(QUERY) 26 | .bind(&web_authentication_challenge.id) 27 | .bind(&web_authentication_challenge.user_name) 28 | .bind(&web_authentication_challenge.passkey) 29 | .fetch_one(database_connection) 30 | .await?; 31 | 32 | Ok(query_result) 33 | } 34 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/update_person_sleep_schedule.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use log::info; 3 | use sqlx::{query, PgPool}; 4 | 5 | use crate::data::entity::{person::PersonId, sleep_schedule::SleepScheduleId}; 6 | 7 | const QUERY: &str = " 8 | INSERT INTO 9 | public.person_sleep_schedule ( 10 | person_id 11 | , sleep_schedule_id 12 | ) 13 | VALUES 14 | ($1, $2) 15 | ;"; 16 | 17 | pub async fn update_person_sleep_schedule_query( 18 | database_connection: &PgPool, 19 | schedule_id: &SleepScheduleId, 20 | person_id: &PersonId, 21 | ) -> Result<()> { 22 | info!("QUERY CALL: update_person_sleep_schedule_query"); 23 | 24 | let updated_row_count: u64 = query(QUERY) 25 | .bind(person_id) 26 | .bind(schedule_id) 27 | .execute(database_connection) 28 | .await? 29 | .rows_affected(); 30 | 31 | if updated_row_count < 1_u64 { 32 | return Err(anyhow!( 33 | "No rows were updated! query: update_person_sleep_schedule_query" 34 | )); 35 | } 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/sleep_schedule/create_sleep_schedule.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | use time::Time; 5 | use uuid::Uuid; 6 | 7 | use crate::data::entity::sleep_schedule::SleepSchedule; 8 | 9 | const QUERY: &str = " 10 | INSERT INTO 11 | public.sleep_schedule ( 12 | id 13 | , start_time 14 | , end_time 15 | ) 16 | VALUES 17 | ($1, $2, $3) 18 | RETURNING 19 | id 20 | , start_time 21 | , end_time 22 | ;"; 23 | 24 | pub async fn create_sleep_schedule_query( 25 | database_connection: &PgPool, 26 | start_time: &Time, 27 | end_time: &Time, 28 | ) -> Result { 29 | info!("QUERY CALL: create_sleep_schedule_query"); 30 | let new_id: Uuid = Uuid::new_v4(); 31 | 32 | let query_result: SleepSchedule = query_as::<_, SleepSchedule>(QUERY) 33 | .bind(new_id) 34 | .bind(start_time) 35 | .bind(end_time) 36 | .fetch_one(database_connection) 37 | .await?; 38 | 39 | Ok(query_result) 40 | } 41 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/person_by_email_address.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use email_address::EmailAddress; 3 | use log::info; 4 | use sqlx::{query_as, PgPool}; 5 | 6 | use crate::data::entity::person::Person; 7 | 8 | const _QUERY: &str = " 9 | SELECT 10 | person.id 11 | , person.email_address_id 12 | , person.registration_date 13 | , person.alias 14 | , person.chronicle_id 15 | FROM 16 | public.person 17 | INNER JOIN 18 | email_address ON person.email_address_id = email_address.id 19 | WHERE 20 | email_address.local_part = $1 21 | AND email_address.domain = $2 22 | LIMIT 23 | 1 24 | ;"; 25 | 26 | pub async fn person_by_email_address_query( 27 | database_connection: &PgPool, 28 | email_address: &EmailAddress, 29 | ) -> Result> { 30 | info!("QUERY CALL: person_by_email_address_query"); 31 | let query_result: Option = query_as::<_, Person>(_QUERY) 32 | .bind(email_address.local_part()) 33 | .bind(email_address.domain()) 34 | .fetch_optional(database_connection) 35 | .await?; 36 | Ok(query_result) 37 | } 38 | -------------------------------------------------------------------------------- /loremaster-web-server/src/utility/constants.rs: -------------------------------------------------------------------------------- 1 | pub mod cookie_fields; 2 | pub mod database; 3 | pub mod files; 4 | pub mod unicode; 5 | 6 | pub const FAILED_LOGIN_MESSAGE: &str = 7 | "Unable to verify your identity with the credentials you've provided."; 8 | pub const SUCCESSFUL_LOGIN_MESSAGE: &str = "User authenticated successfully!"; 9 | 10 | pub const REGISTRATION_SUCCESS_MESSAGE: &str = "Account created successfully!"; 11 | pub const INVALID_EMAIL_MESSAGE: &str = "Invalid email address!"; 12 | pub const INVALID_PASSWORD_MESSAGE: &str = "Invalid password!"; 13 | pub const BLOCKED_EMAIL_MESSAGE: &str = 14 | "Registration is currently closed as the application is not ready for public users yet. Sorry!"; 15 | 16 | // pub const FRONTEND_ORIGIN_URL: &str = "http://127.0.0.1:"; 17 | pub const LOREMASTER_CONFIGURATION_FILE_PATH: &str = "./Loremaster.ron"; 18 | pub const RELAYING_PARTY_ID: &str = "chronilore.day"; 19 | pub const DEV_RELAYING_PARTY_ID: &str = "chronilore.day"; 20 | pub const QA_RELAYING_PARTY_ID: &str = "chronilore.day"; 21 | pub const LOCAL_HOST_RELAYING_PARTY_ID: &str = "localhost"; 22 | pub const RELAYING_PARTY: &str = "Loremaster"; 23 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/navigation/tab/tab_section.rs: -------------------------------------------------------------------------------- 1 | use sycamore::prelude::*; 2 | 3 | use crate::components::navigation::tab::tab_panel::TabIndex; 4 | 5 | #[derive(Prop)] 6 | pub struct TabSectionProperties<'tab, G: Html> { 7 | pub title: String, 8 | pub index: TabIndex, 9 | pub classes: &'tab ReadSignal, 10 | pub children: Children<'tab, G>, 11 | } 12 | 13 | #[component] 14 | pub fn TabSection<'tab, G: Html>( 15 | context: Scope<'tab>, 16 | TabSectionProperties { 17 | title, 18 | index, 19 | classes, 20 | children, 21 | }: TabSectionProperties<'tab, G>, 22 | ) -> View { 23 | let active_tab: &Signal = use_context::>(context); 24 | 25 | let children: View = children.call(context); 26 | view! {context, 27 | (match active_tab.get().as_ref() == &index { 28 | true => { 29 | view! {context, div(class=(format!("tab-section {}", classes.get())), id="") { 30 | (children) 31 | }} 32 | }, 33 | false => view! {context, ""}, 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /loremaster-web-server/src/security/sanitization.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | pub fn character_is_invisible_unicode(input: &char) -> bool { 4 | *input as u32 > 0x7F 5 | } 6 | 7 | pub fn remove_invisible_characters(input: String) -> Result { 8 | Ok(input 9 | .chars() 10 | .filter(|character| !character_is_invisible_unicode(character)) 11 | .collect()) 12 | } 13 | 14 | pub fn sanitize_user_input_string(mut input: String) -> Result { 15 | input = remove_invisible_characters(input)?; 16 | Ok(input.chars().collect()) 17 | } 18 | 19 | #[cfg(test)] 20 | mod tests { 21 | use anyhow::Result; 22 | 23 | use crate::utility::constants::unicode; 24 | 25 | use super::sanitize_user_input_string; 26 | 27 | #[test] 28 | fn verify_whitespace() -> Result<()> { 29 | assert!( 30 | sanitize_user_input_string(String::from(unicode::invisible::_LEFT_TO_RIGHT_MARK))? 31 | .is_empty() 32 | ); 33 | assert!(sanitize_user_input_string(String::from( 34 | unicode::invisible::_CHARACTER_TABULATION 35 | ))? 36 | .is_empty()); 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/authentication/add_web_authentication_register.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | 5 | use crate::data::entity::web_authentication_challenge::WebAuthenticationRegister; 6 | 7 | const QUERY: &str = " 8 | INSERT INTO 9 | public.web_authentication_register ( 10 | id 11 | , user_name 12 | , passkey 13 | ) 14 | VALUES 15 | ($1, $2, $3) 16 | RETURNING 17 | id 18 | , user_name 19 | , passkey 20 | ;"; 21 | 22 | pub async fn add_web_authentication_register_query( 23 | database_connection: &PgPool, 24 | web_authentication_register: &WebAuthenticationRegister, 25 | ) -> Result { 26 | info!("QUERY CALL: add_web_authentication_register_query"); 27 | let query_result: WebAuthenticationRegister = query_as::<_, WebAuthenticationRegister>(QUERY) 28 | .bind(&web_authentication_register.id) 29 | .bind(&web_authentication_register.user_name) 30 | .bind(&web_authentication_register.passkey) 31 | .fetch_one(database_connection) 32 | .await?; 33 | 34 | Ok(query_result) 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: This is a template for the loremaster team to use for feature tracking. 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Issue Template 11 | 12 | ## Description 13 | 14 | Here is a high level description of the issue. 15 | 16 | ## Purpose 17 | 18 | What is the point of this 19 | 20 | ## Resources 21 | 22 | A list of resources, can be links 23 | 24 | - [first resource](https://github.com/seanpmyers/loremaster) 25 | - [second resource](https://github.com/seanpmyers/loremaster) 26 | - [third resource](https://github.com/seanpmyers/loremaster) 27 | 28 | ## Version 1 29 | 30 | ### Features 31 | 32 | - [x] Issue link 33 | - [x] Issue link 34 | - [x] Issue link 35 | 36 | ### Pull Requests 37 | 38 | - [Link](https://github.com/seanpmyers/loremaster) 39 | 40 | ## Version 2 41 | 42 | - [ ] Issue link 43 | - [ ] Issue link 44 | - [ ] Issue link 45 | 46 | ## Options 47 | 48 | This is where a list of options can be created. 49 | 50 | - [x] [option](https://github.com/seanpmyers/loremaster) 51 | - [ ] [other option](https://github.com/seanpmyers/loremaster) 52 | - [ ] [another option](https://github.com/seanpmyers/loremaster) 53 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/container.rs: -------------------------------------------------------------------------------- 1 | use perseus::reactor::Reactor; 2 | use sycamore::{futures::spawn_local_scoped, prelude::*}; 3 | 4 | use crate::{ 5 | components::navigation::{side_nav_bar::SideNavBar, top_nav_bar::TopNavBar}, 6 | global_state::ApplicationStateRx, 7 | }; 8 | 9 | #[derive(Prop)] 10 | pub struct ContainerProperties<'a, G: Html> { 11 | pub title: &'a str, 12 | pub children: Children<'a, G>, 13 | } 14 | 15 | #[component] 16 | pub fn Container<'a, G: Html>( 17 | context: Scope<'a>, 18 | ContainerProperties { title, children }: ContainerProperties<'a, G>, 19 | ) -> View { 20 | let user_authentication = 21 | Reactor::::from_cx(context).get_global_state::(context); 22 | let children: View = children.call(context); 23 | if G::IS_BROWSER { 24 | spawn_local_scoped(context, async { 25 | user_authentication.authentication.detect_state().await; 26 | }); 27 | } 28 | view! {context, 29 | div(class="glass container") { 30 | TopNavBar() 31 | SideNavBar() 32 | div(class="", id="loremaster-main", data-title=title) { 33 | (children) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /infrastructure/google_cloud_platform/terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/google" { 5 | version = "3.5.0" 6 | constraints = "3.5.0" 7 | hashes = [ 8 | "h1:g1dSPkP3+HREeNKoGGRVqeWMl5AonGy8MtOpuhr3rGc=", 9 | "zh:1bd907d58a05ec25d48294f6a0d7ab215d7243daa8a100ee983981ca641d2ce9", 10 | "zh:334c0ac7599da444434d5cd1045171b8decf4eec1d01a69dd9a343e62ca8ffc0", 11 | "zh:738c0808e29d14e9508f121c1966fa557b3db83fb5b0ce9b32ffebfbcc610dbe", 12 | "zh:8b4c910d558d3176f96c50bca9c3484edd0df2a7429bf1609cd5aca2cf1aeab8", 13 | "zh:a1e3d0057158267c2d86a5fc46cfa50e86cf70a60f242a96c66710c48d97bc04", 14 | "zh:a718d621c671cb6d1d951e070ed9f52710a2441fa14e1aee7c00e14396142f9a", 15 | "zh:ac1381f6e42de469a2842af8ba110787c9a7c1a1b250a53fe0831b70f54a56f6", 16 | "zh:af979d49936061c0c37c1df7ddfbf1bca6b1f347562c8917b4e063ac7c354af7", 17 | "zh:af9a7e94706d4155f88b2547620edc2ebfeaf2c4bb8f670a75e04ab7470c6640", 18 | "zh:d820fb164d3f7da15aa4963a59f05134362b28c983d0cfc30ee3e325bc3362cb", 19 | "zh:e1cd06c6ed45dd0d644b4afec7b8499e7be6337d45f4b802c933dd0016efc326", 20 | "zh:fe6e7ea5f1a1d7876ac9939eb8cfc1376536d25ad7ecc86f5d6660c5b09a9de3", 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/credential_by_email_address.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use email_address::EmailAddress; 3 | use log::info; 4 | use sqlx::{query_as, PgPool}; 5 | 6 | use crate::data::entity::person::Credentials; 7 | 8 | const QUERY: &str = " 9 | SELECT 10 | person.id 11 | , email_address.display as email_address 12 | , password.encrypted_password 13 | FROM 14 | public.email_address 15 | INNER JOIN 16 | public.person ON public.email_address.id = person.email_address_id 17 | INNER JOIN 18 | public.person_password ON person.id = person_password.person_id 19 | INNER JOIN 20 | public.password ON person_password.password_id = password.id 21 | WHERE 22 | email_address.local_part = $1 23 | AND email_address.domain = $2 24 | LIMIT 25 | 1 26 | ;"; 27 | 28 | pub async fn credential_by_email_address_query( 29 | database_connection: &PgPool, 30 | email_address: &EmailAddress, 31 | ) -> Result> { 32 | info!("QUERY CALL: credential_by_email_address_query"); 33 | let query_result = query_as::<_, Credentials>(QUERY) 34 | .bind(email_address.local_part()) 35 | .bind(email_address.domain()) 36 | .fetch_optional(database_connection) 37 | .await?; 38 | 39 | Ok(query_result) 40 | } 41 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/authentication/get_webauthn_passkeys_by_email_address.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use email_address::EmailAddress; 3 | use serde_json::Value; 4 | use sqlx::{query_scalar, PgPool}; 5 | use webauthn_rs::prelude::Passkey; 6 | 7 | const QUERY: &str = " 8 | SELECT 9 | passkey 10 | FROM 11 | email_address 12 | INNER JOIN person ON person.email_address_id = email_address .id 13 | INNER JOIN person_web_authentication_key ON person.id = person_web_authentication_key.person_id 14 | INNER JOIN web_authentication_key ON person_web_authentication_key.web_authentication_key_id = web_authentication_key.id 15 | WHERE 16 | email_address.local_part = $1 17 | AND email_address.domain = $2 18 | ;"; 19 | 20 | pub async fn get_webauthn_passkeys_by_email_address_query( 21 | database_connection: &PgPool, 22 | email_address: &EmailAddress, 23 | ) -> Result> { 24 | let query_result: Vec = query_scalar(QUERY) 25 | .bind(email_address.local_part()) 26 | .bind(email_address.domain()) 27 | .fetch_all(database_connection) 28 | .await?; 29 | 30 | Ok(query_result 31 | .into_iter() 32 | .map(|json| { 33 | let key: Passkey = serde_json::from_value(json).unwrap(); 34 | key 35 | }) 36 | .collect::>()) 37 | } 38 | -------------------------------------------------------------------------------- /loremaster-web-server/README.md: -------------------------------------------------------------------------------- 1 | # loremaster - Backend 2 | 3 | The application is primarily written in Rust. 4 | 5 | ## Tech Stack 6 | 7 | - Programming Languages 8 | - [Rust](https://www.rust-lang.org/) 9 | - [SQL](https://en.wikipedia.org/wiki/SQL) 10 | - Frameworks 11 | - Rust 12 | - [Axum](https://github.com/tokio-rs/axum) 13 | - [SQLx](https://github.com/launchbadge/sqlx) 14 | - [Sycamore](https://github.com/sycamore-rs/sycamore) 15 | - [Perseus](https://github.com/framesurge/perseus) 16 | - Databases / Datastores 17 | - [PostgreSQL](https://www.postgresql.org/) 18 | - Development Operations 19 | - Cloud Service Provider 20 | - [AWS](https://aws.amazon.com/) 21 | - Version Control 22 | - [Git](https://git-scm.com/) 23 | - [GitHub](https://github.com/) 24 | - [Docker](https://www.docker.com/) 25 | - Observability/Logging 26 | 27 | ### Database 28 | 29 | Docker compose command 30 | 31 | ```sh 32 | docker-compose -f ../database/docker-compose.yml up 33 | ``` 34 | 35 | You can generate a site secret for testing with the following openssl command: 36 | 37 | ```bash 38 | openssl rand -base64 64 39 | ``` 40 | 41 | Generate SSL/TLS certs 42 | 43 | (rename to cert.pem/key.pem) 44 | 45 | ```bash 46 | openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -sha256 -nodes --subj '/CN=localhost/' 47 | ``` 48 | -------------------------------------------------------------------------------- /loremaster-database/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use loremaster::data::postgres_handler::PostgresHandler; 3 | use sqlx::PgPool; 4 | 5 | const CONNECTION_STRING_ENVIRONMENT_VARIABLE_KEY: &str = "POSTGRES_CONNECTION_STRING"; 6 | const DATABASE_FOLDER_PATH: &str = "../database/"; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | println!("Starting up..."); 11 | 12 | let postgres_connection_string: String = 13 | std::env::var(CONNECTION_STRING_ENVIRONMENT_VARIABLE_KEY) 14 | .expect("Missing postgresql connection string."); 15 | 16 | if postgres_connection_string.is_empty() { 17 | panic!("Postgresql connection string is empty!"); 18 | } 19 | 20 | if !std::path::Path::new(DATABASE_FOLDER_PATH).exists() { 21 | panic!("Database folder path does not exist!"); 22 | } 23 | 24 | let postgres_handler: PostgresHandler = 25 | PostgresHandler::new(postgres_connection_string).await?; 26 | 27 | ping(&postgres_handler.database_pool).await?; 28 | 29 | Ok(()) 30 | } 31 | 32 | pub async fn ping(database_pool: &PgPool) -> Result<()> { 33 | const PING_QUERY: &str = "SELECT 1;"; 34 | 35 | let rows_affected: u64 = sqlx::query(PING_QUERY) 36 | .execute(database_pool) 37 | .await? 38 | .rows_affected(); 39 | 40 | println!("{}", rows_affected); 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/create_person.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | use time::OffsetDateTime; 5 | use uuid::Uuid; 6 | 7 | use crate::data::entity::person::Person; 8 | 9 | const QUERY: &str = " 10 | INSERT INTO 11 | public.person ( 12 | id 13 | , email_address_id 14 | , registration_date 15 | , alias 16 | , chronicle_id 17 | ) 18 | VALUES 19 | ($1, $2, $3, $4, $5) 20 | RETURNING 21 | id 22 | , email_address_id 23 | , registration_date 24 | , alias 25 | , chronicle_id 26 | ;"; 27 | 28 | pub async fn create_person_query( 29 | database_connection: &PgPool, 30 | email_address_id: &Uuid, 31 | alias: Option<&str>, 32 | chronicle_id: Option, 33 | ) -> Result { 34 | info!("QUERY CALL: create_person_query"); 35 | let new_person_id: Uuid = Uuid::new_v4(); 36 | let creation_date: OffsetDateTime = OffsetDateTime::now_utc(); 37 | 38 | let query_result: Person = query_as::<_, Person>(QUERY) 39 | .bind(new_person_id) 40 | .bind(email_address_id) 41 | .bind(creation_date) 42 | .bind(alias) 43 | .bind(chronicle_id) 44 | .fetch_one(database_connection) 45 | .await?; 46 | 47 | Ok(query_result) 48 | } 49 | -------------------------------------------------------------------------------- /loremaster-web-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time::Duration}; 2 | 3 | use anyhow::Result; 4 | use configuration::application::LoremasterWebServerConfiguration; 5 | use log::info; 6 | 7 | mod api; 8 | mod configuration; 9 | mod data; 10 | mod security; 11 | mod utility; 12 | 13 | #[tokio::main] 14 | async fn main() -> Result<()> { 15 | info!("Starting up!"); 16 | let configuration: LoremasterWebServerConfiguration = 17 | configuration::application::configure().await?; 18 | 19 | match configuration.scheduler_state { 20 | configuration::application::SchedulerState::On => { 21 | let (web_server_result, scheduler_result) = tokio::join!( 22 | tokio::spawn(async move { api::web_server::start(configuration).await }), 23 | tokio::spawn(async move { scheduler().await }) 24 | ); 25 | 26 | web_server_result.unwrap_err(); 27 | scheduler_result.unwrap_err(); 28 | } 29 | configuration::application::SchedulerState::Off => { 30 | api::web_server::start(configuration).await? 31 | } 32 | } 33 | 34 | info!("Shutting down."); 35 | 36 | Ok(()) 37 | } 38 | 39 | async fn scheduler() -> Result<()> { 40 | loop { 41 | thread::sleep(Duration::from_secs(2)); 42 | info!("Checking schedules..."); 43 | } 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /documentation/tracking/work_session.md: -------------------------------------------------------------------------------- 1 | # Work Session 2 | 3 | | Subject | Date | Length | Notes | 4 | |---|---|---|---| 5 | | WebAuthn |2023-06-19| 45 minutes | Implemented functional registration (back-end start and finish routes/handlers and front-end) | 6 | | WebAuthn |2023-06-19| 91 minutes | Implemented functional login (back-end start and finish routes/handlers) | 7 | | WebAuthn |2023-06-23| 62 minutes | Updating webauthn front-end login form and fixed some things with back-end api handlers/queries. Basic registration and login with webauthn functioning. | 8 | | Project Management |2023-06-24| 60 minutes | Refining the process of issue tracking using GitHub. | 9 | | RON Configuration |2023-06-24| 58 minutes | Updating application configuration to use ron file. | 10 | | RON Configuration |2023-06-26| 9 minutes | Removing old configuration implementation | 11 | | Website grid layout |2023-07-04| 61 minutes | Changing the layout from flex to a grid | 12 | | Website grid layout |2023-07-06| 90 minutes | Updating general styling of nav bars and main content area. | 13 | | Site styling |2023-07-07| 40 minutes | Updating styling | 14 | | Site styling |2023-07-09| 240 minutes | Trying out glass styling and updating timeline page. | 15 | | Site styling |2023-07-13| 60 minutes | Updating glass styling and updating you page. | 16 | | Website UI |2023-08-04| 120 minutes | Updating modal functionality. | 17 | -------------------------------------------------------------------------------- /loremaster/src/data/postgres_handler.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use sqlx::{pool::PoolOptions, postgres::PgPoolOptions, PgPool}; 3 | use std::time::Duration; 4 | 5 | const DB_POOL_MAX_OPEN: u32 = 32; 6 | const DB_POOL_MAX_IDLE: u64 = 8; 7 | const DB_POOL_TIMEOUT_SECONDS: u64 = 15; 8 | 9 | #[derive(Clone)] 10 | pub struct PostgresHandler { 11 | pub connection_string: String, 12 | pub database_pool: PgPool, 13 | } 14 | 15 | impl PostgresHandler { 16 | pub async fn new(connection_string: String) -> Result { 17 | let database_pool: PgPool = create_database_pool(&connection_string) 18 | .await 19 | .map_err(|error| anyhow!("{}", error))?; 20 | 21 | let new_handler: PostgresHandler = PostgresHandler { 22 | connection_string, 23 | database_pool, 24 | }; 25 | 26 | Ok(new_handler) 27 | } 28 | } 29 | 30 | pub async fn create_database_pool(connection_string: &str) -> Result { 31 | let options: PoolOptions = PgPoolOptions::new() 32 | .idle_timeout(Duration::from_secs(DB_POOL_MAX_IDLE)) 33 | .max_connections(DB_POOL_MAX_OPEN) 34 | .acquire_timeout(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS)); 35 | 36 | let result: PgPool = options 37 | .connect(connection_string) 38 | .await 39 | .map_err(|error| anyhow!("{}", error))?; 40 | 41 | Ok(result) 42 | } 43 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/navigation/tab/tab_button.rs: -------------------------------------------------------------------------------- 1 | use sycamore::prelude::*; 2 | use web_sys::Event; 3 | 4 | use crate::components::{icon::SvgIcon, navigation::tab::tab_panel::TabIndex}; 5 | 6 | #[derive(Prop)] 7 | pub struct TabButtonProperties<'tab> { 8 | pub title: String, 9 | pub index: TabIndex, 10 | pub classes: &'tab Signal, 11 | pub icon: Option, 12 | } 13 | 14 | #[component] 15 | pub fn TabButton<'tab, G: Html>( 16 | context: Scope<'tab>, 17 | TabButtonProperties { 18 | title, 19 | index, 20 | classes, 21 | icon, 22 | }: TabButtonProperties<'tab>, 23 | ) -> View { 24 | let active_tab: &Signal = use_context::>(context); 25 | let clicked = move |event: Event| { 26 | event.prevent_default(); 27 | create_effect(context, move || active_tab.set(index)); 28 | }; 29 | 30 | view! {context, 31 | button(class=(match active_tab.get().as_ref() == &index { 32 | true => format!("tab-button active-tab {}", classes.get()), 33 | false => format!("tab-button {}", classes), 34 | }), id="", on:click=clicked) { 35 | (match icon { 36 | Some(icon) => view!{context, span(dangerously_set_inner_html=icon) {} }, 37 | None => view!{context, }, 38 | }) 39 | (title) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/postgres_handler.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use sqlx::{pool::PoolOptions, postgres::PgPoolOptions, PgPool}; 3 | use std::time::Duration; 4 | 5 | const DB_POOL_MAX_OPEN: u32 = 32; 6 | const DB_POOL_MAX_IDLE: u64 = 8; 7 | const DB_POOL_TIMEOUT_SECONDS: u64 = 15; 8 | 9 | #[derive(Clone)] 10 | pub struct PostgresHandler { 11 | pub connection_string: String, 12 | pub database_pool: PgPool, 13 | } 14 | 15 | impl PostgresHandler { 16 | pub async fn new(connection_string: String) -> Result { 17 | let database_pool: PgPool = create_database_pool(&connection_string) 18 | .await 19 | .map_err(|error| anyhow!("{}", error))?; 20 | 21 | let new_handler: PostgresHandler = PostgresHandler { 22 | connection_string, 23 | database_pool, 24 | }; 25 | 26 | Ok(new_handler) 27 | } 28 | } 29 | 30 | pub async fn create_database_pool(connection_string: &str) -> Result { 31 | let options: PoolOptions = PgPoolOptions::new() 32 | .idle_timeout(Duration::from_secs(DB_POOL_MAX_IDLE)) 33 | .max_connections(DB_POOL_MAX_OPEN) 34 | .acquire_timeout(Duration::from_secs(DB_POOL_TIMEOUT_SECONDS)); 35 | 36 | let result: PgPool = options 37 | .connect(connection_string) 38 | .await 39 | .map_err(|error| anyhow!("{}", error))?; 40 | 41 | Ok(result) 42 | } 43 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/entity/person.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use sqlx::types::time::OffsetDateTime; 3 | use uuid::Uuid; 4 | 5 | use super::email_address::EmailAddressId; 6 | 7 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct Person { 10 | pub id: PersonId, 11 | pub email_address_id: EmailAddressId, 12 | pub registration_date: OffsetDateTime, 13 | pub alias: Option, 14 | pub chronicle_id: Option, 15 | } 16 | 17 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::Decode, sqlx::Encode)] 18 | pub struct PersonId(pub Uuid); 19 | 20 | impl sqlx::Type for PersonId { 21 | fn type_info() -> ::TypeInfo { 22 | >::type_info() 23 | } 24 | } 25 | 26 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 27 | #[serde(rename_all = "camelCase")] 28 | pub struct Credentials { 29 | pub id: PersonId, 30 | pub email_address: String, 31 | pub encrypted_password: String, 32 | } 33 | 34 | #[derive(Deserialize, Serialize, Debug, Clone, sqlx::FromRow)] 35 | #[serde(rename_all = "camelCase")] 36 | pub struct PersonMeta { 37 | pub id: PersonId, 38 | pub email_address: String, 39 | pub registration_date: OffsetDateTime, 40 | pub alias: Option, 41 | } 42 | 43 | // pub struct SessionKey (String); 44 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/utility/date_time_helper.rs: -------------------------------------------------------------------------------- 1 | use super::constants::{ 2 | APRIL, AUGUST, DECEMBER, FEBRUARY, JANUARY, JULY, JUNE, MARCH, MAY, NOVEMBER, OCTOBER, 3 | SEPTEMBER, 4 | }; 5 | 6 | pub fn get_day_of_week_from_integer(day_number: u32) -> String { 7 | match day_number { 8 | 0 => String::from("Sunday"), 9 | 1 => String::from("Monday"), 10 | 2 => String::from("Tuesday"), 11 | 3 => String::from("Wednesday"), 12 | 4 => String::from("Thursday"), 13 | 5 => String::from("Friday"), 14 | 6 => String::from("Saturday"), 15 | _ => { 16 | log::error!("Invalid day of week integer!"); 17 | String::from("") 18 | } 19 | } 20 | } 21 | 22 | pub fn get_month_from_integer(month_number: u32) -> String { 23 | match month_number { 24 | 0 => String::from(JANUARY), 25 | 1 => String::from(FEBRUARY), 26 | 2 => String::from(MARCH), 27 | 3 => String::from(APRIL), 28 | 4 => String::from(MAY), 29 | 5 => String::from(JUNE), 30 | 6 => String::from(JULY), 31 | 7 => String::from(AUGUST), 32 | 8 => String::from(SEPTEMBER), 33 | 9 => String::from(OCTOBER), 34 | 10 => String::from(NOVEMBER), 35 | 11 => String::from(DECEMBER), 36 | _ => { 37 | log::error!("Invalid month integer"); 38 | String::from("") 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /loremaster-web-server/src/api/guards/user.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | async_trait, 3 | extract::{FromRef, FromRequestParts}, 4 | http::{request::Parts, StatusCode}, 5 | }; 6 | use axum_extra::extract::{cookie::Key, PrivateCookieJar}; 7 | use serde::{Deserialize, Serialize}; 8 | use uuid::Uuid; 9 | 10 | use crate::utility::constants::cookie_fields::USER_ID; 11 | 12 | #[derive(Serialize, Deserialize, Debug, Clone, Copy)] 13 | pub struct User(pub Uuid); 14 | // TODO: check database for user id/session id 15 | #[async_trait] 16 | impl FromRequestParts for User 17 | where 18 | S: Send + Sync, 19 | Key: FromRef, 20 | { 21 | type Rejection = (StatusCode, &'static str); 22 | async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { 23 | let cookie_result = PrivateCookieJar::::from_request_parts(parts, state).await; 24 | match cookie_result { 25 | Ok(cookie_jar) => match cookie_jar.get(USER_ID) { 26 | Some(cookie) => { 27 | let user_id = Uuid::parse_str(cookie.value()).unwrap(); 28 | Ok(User(user_id)) 29 | } 30 | None => Err((StatusCode::UNAUTHORIZED, "No authorization found!")), 31 | }, 32 | Err(_) => Err(( 33 | StatusCode::INTERNAL_SERVER_ERROR, 34 | "Failed to evaluate authorization.", 35 | )), 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person/update_meta_by_id.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use sqlx::{query, query_as, PgPool}; 3 | use uuid::Uuid; 4 | 5 | use crate::data::entity::person::PersonMeta; 6 | 7 | const QUERY: &str = " 8 | UPDATE 9 | person 10 | SET 11 | alias = $2 12 | WHERE 13 | id = $1 14 | ;"; 15 | 16 | const RESULT_QUERY: &str = " 17 | SELECT 18 | person.id 19 | , email_address.display as email_address 20 | , person.registration_date 21 | , person.alias 22 | FROM 23 | public.person 24 | INNER JOIN 25 | public.email_address ON person.email_address_id = email_address.id 26 | WHERE 27 | person.id = $1 28 | LIMIT 29 | 1 30 | ;"; 31 | 32 | pub async fn update_meta_by_id_query( 33 | database_connection: &PgPool, 34 | person_id: &Uuid, 35 | alias: &str, 36 | ) -> Result { 37 | let updated_row_count: u64 = query(QUERY) 38 | .bind(person_id) 39 | .bind(alias) 40 | .execute(database_connection) 41 | .await? 42 | .rows_affected(); 43 | 44 | if updated_row_count < 1_u64 { 45 | return Err(anyhow!( 46 | "No rows were updated! query: update_meta_by_id_query" 47 | )); 48 | } 49 | 50 | let query_result: PersonMeta = query_as::<_, PersonMeta>(RESULT_QUERY) 51 | .bind(person_id) 52 | .fetch_one(database_connection) 53 | .await?; 54 | Ok(query_result) 55 | } 56 | -------------------------------------------------------------------------------- /database/initalization/setup.sql: -------------------------------------------------------------------------------- 1 | -- Enums 2 | 3 | \i database/enum/weekday.sql 4 | 5 | -- Entity Tables 6 | \i database/table/email_address.sql 7 | \i database/table/person.sql 8 | \i database/table/chronicle.sql 9 | \i database/table/action.sql 10 | \i database/table/intention.sql 11 | \i database/table/goal.sql 12 | \i database/table/principle.sql 13 | \i database/table/sleep_schedule.sql 14 | \i database/table/skill.sql 15 | \i database/table/dictionary.sql 16 | \i database/table/term.sql 17 | \i database/table/schedule.sql 18 | \i database/table/web_authentication_register.sql 19 | \i database/table/web_authentication_login.sql 20 | \i database/table/web_authentication_key.sql 21 | \i database/table/password.sql 22 | 23 | -- Relation Tables 24 | \i database/table/person_action.sql 25 | \i database/table/person_goal.sql 26 | \i database/table/person_email_address.sql 27 | \i database/table/person_principle.sql 28 | \i database/table/person_sleep_schedule.sql 29 | \i database/table/person_skill.sql 30 | \i database/table/dictionary_term.sql 31 | \i database/table/schedule_weekday.sql 32 | \i database/table/intention_instance.sql 33 | \i database/table/person_intention_schedule.sql 34 | \i database/table/person_dictionary.sql 35 | \i database/table/person_password.sql 36 | \i database/table/person_web_authentication_key.sql 37 | 38 | 39 | -- Relations 40 | 41 | ALTER TABLE "person" 42 | ADD 43 | CONSTRAINT "chronicle_foreign_key" FOREIGN KEY ("chronicle_id") REFERENCES "chronicle" ("id"); 44 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/notification/toast.rs: -------------------------------------------------------------------------------- 1 | use sycamore::prelude::*; 2 | 3 | use crate::components::{ 4 | icon::{ALERT_OCTAGON_SVG_HTML, ALERT_TRIANGLE_SVG_HTML, CHECK_SVG_HTML, INFO_SVG_HTML}, 5 | state::message_type::MessageType, 6 | }; 7 | 8 | #[derive(Prop)] 9 | pub struct ToastProperties<'a> { 10 | pub content: &'a ReadSignal, 11 | pub message_type: &'a ReadSignal, 12 | } 13 | 14 | #[component] 15 | pub fn Toast<'a, 'b: 'a, G: Html>(context: Scope<'a>, properties: ToastProperties<'b>) -> View { 16 | view! { context, 17 | div( 18 | class=(match *properties.message_type.get() { 19 | MessageType::Information => "toast", 20 | MessageType::Success => "toast toast--success", 21 | MessageType::Error => "toast toast--error", 22 | MessageType::Warning => "toast toast--warning" 23 | }) 24 | ) { 25 | span( 26 | class="toast-icon", 27 | dangerously_set_inner_html=match *properties.message_type.get() { 28 | MessageType::Information => INFO_SVG_HTML, 29 | MessageType::Success => CHECK_SVG_HTML, 30 | MessageType::Error => ALERT_OCTAGON_SVG_HTML, 31 | MessageType::Warning => ALERT_TRIANGLE_SVG_HTML 32 | } 33 | ) { 34 | 35 | } 36 | span(class="toast-content") { (properties.content.get()) } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/rust-clippy.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # rust-clippy is a tool that runs a bunch of lints to catch common 6 | # mistakes in your Rust code and help improve your Rust code. 7 | # More details at https://github.com/rust-lang/rust-clippy 8 | # and https://rust-lang.github.io/rust-clippy/ 9 | 10 | name: rust-clippy analyze 11 | 12 | on: 13 | push: 14 | branches: [ "main" ] 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [ "main" ] 18 | schedule: 19 | - cron: '23 21 * * 3' 20 | 21 | jobs: 22 | rust-clippy-analyze: 23 | name: Run rust-clippy analyzing 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: read 27 | security-events: write 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v3 31 | 32 | - name: Install Rust toolchain 33 | uses: dtolnay/rust-toolchain@master 34 | with: 35 | toolchain: stable 36 | components: clippy 37 | 38 | - name: Install required cargo 39 | run: cargo install clippy-sarif sarif-fmt 40 | 41 | - name: Run rust-clippy 42 | run: 43 | cargo clippy 44 | --all-features 45 | --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt 46 | continue-on-error: true 47 | 48 | - name: Upload analysis results to GitHub 49 | uses: github/codeql-action/upload-sarif@v2 50 | with: 51 | sarif_file: rust-clippy-results.sarif 52 | wait-for-processing: true 53 | -------------------------------------------------------------------------------- /documentation/entity/person.md: -------------------------------------------------------------------------------- 1 | # Person 2 | 3 | ## Description 4 | 5 | User of the application. 6 | 7 | The main focus of the application is the user. 8 | Ideally the app can help the user plan, document, and track their life. 9 | 10 | It is difficult to determine what is the right amount of information to link to individuals in the app. 11 | Ideally this application would maximize privacy of it's users, but given the nature of the application it may be difficult. 12 | Encrypting all user's data using their password might be possible, although the technical limitations that might impose are unknown as of writing this. 13 | This application's functionality and utility require the user to provide detailed private and personal information. 14 | Being able to leverage user data to improve the quality and functionality of the application is also a possibility that must be considered. 15 | Certain data entities like actions, schedules, and intentions can possibly be reused by other users. This would reduce the overall required effort by individual users. 16 | 17 | - Culture 18 | - Philosophy 19 | - Values 20 | - Beliefs 21 | - Principles/Virtues 22 | - Language 23 | - Customs and Traditions 24 | - Social Rules? 25 | - Capabilities 26 | - Skills 27 | - Habits 28 | - Actions 29 | - Routines 30 | - Intentions 31 | - Knowledge 32 | - Concepts 33 | - Protocols 34 | - Experiences 35 | - Events 36 | - Places 37 | - Interests 38 | 39 | ## Attributes 40 | 41 | - Email Address 42 | - Password 43 | - Date of Birth (?) 44 | - Username/Alias (?) 45 | - Real Name (?) 46 | 47 | ## Related Entities 48 | 49 | - [Action](./action.md) 50 | - [Chronicle](./chronicle.md) 51 | - [Document](./document.md) 52 | - [Intention](./intention.md) 53 | -------------------------------------------------------------------------------- /documentation/entity/lore.md: -------------------------------------------------------------------------------- 1 | # Lore 2 | 3 | ## Definition 4 | 5 | > a body of traditions and knowledge on a subject or held by a particular group, typically passed from person to person by word of mouth. 6 | 7 | Lore is an abstraction that refers to the majority of the information you record through the application. 8 | 9 | This includes actions, documents, protocols, daily chronicles, etc. 10 | 11 | I think people are the culmination of all the different things they think, do, write, and say. 12 | 13 | So in that way lore is all the information that makes up the person you are or want to be. 14 | 15 | ## Types 16 | 17 | - Document 18 | - A written record that contains the following: 19 | - Description: a written explanation of what something is 20 | - Tags/Links: A list of related concepts 21 | - Protocol/procedure 22 | - Written steps that a person can follow to perform or complete and action or a routine 23 | - Action 24 | - A thing done/enacted by a person 25 | - Routine 26 | - A collection of actions enacted by a person within a given time frame 27 | - Value 28 | - a person's principles or standards of behavior; one's judgment of what is important in life 29 | - Goal 30 | - Some state of being a person seeks to reach 31 | - Achievement 32 | - Some result of an action or actions that a person finds meaningful or significant 33 | - Belief/opinion 34 | - Information that a person thinks is true without certainty 35 | - Habit 36 | - An action or routine that a person tends to enact frequently 37 | - Concept/idea/abstraction 38 | - Name: A term or terms which are used to refer to something a person experiences 39 | - Context: Any additional information/concepts that are required to understand the concept 40 | - Forms: Any alternative names or context that the concept refers to 41 | - Action 42 | - Object 43 | -------------------------------------------------------------------------------- /documentation/user story.md: -------------------------------------------------------------------------------- 1 | # User Story 2 | 3 | A *person* (user) opens the application. 4 | 5 | The *person* starts with some *intention* they have (an objective or goal). 6 | 7 | Intention: Get a new Job 8 | 9 | Plan the *actions* they must take to enact their intention. 10 | *Actions* can have *contingent actions* (basically sub tasks) that must be completed first. 11 | They can record an optional *outcome* for every action they take (what was the result of the action). 12 | For example if they complete and interview, they might be rejected which is an outcome or result. 13 | 14 | Actions: 15 | 16 | - Apply 17 | - Send Resume 18 | - Update Resume 19 | - Create Resume 20 | - Send Cover Letter 21 | - Write Cover Letter 22 | - Interview 23 | - Schedule date/time with company contact 24 | - Prepare 25 | - Practice 26 | - Leetcode 27 | - Whiteboarding 28 | - Study Role Topics 29 | - Read about company 30 | - Read about role 31 | - Accept Offer 32 | - Reject Offer 33 | 34 | A *person* can create documentation for any subject matter they need with a *document*. 35 | A *document* is a record with text written by the *person*. 36 | 37 | A *Chronicle* is a log or recording of your daily actions and *documents*. 38 | *Chronicles* are the basic way to track progress of a *person's* *actions* and progress towards their *intentions*. 39 | *Chronicles* are auto generated every day. There cannot be more than one chronicle for a given day. 40 | 41 | When *actions* have been documented, they can then be planned with a *schedule*. 42 | This allows a *person* to set *actions* they want to complete on set intervals, for example they could plan daily actions to brush their teeth or practice leetcode. 43 | These *scheduled* *actions* will appear on relevant *chronicles* to help the user track their activity. -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/frequency_options.rs: -------------------------------------------------------------------------------- 1 | use perseus::prelude::spawn_local_scoped; 2 | use sycamore::prelude::*; 3 | 4 | use crate::{ 5 | data::entity::frequency::Frequency, 6 | utility::{ 7 | constants::{API_BASE_URL, API_FREQUENCY_LIST_ROUTE}, 8 | http_service, 9 | }, 10 | }; 11 | 12 | pub struct FrequencyOptionsProperties {} 13 | 14 | #[component] 15 | pub fn FrequencyOptions( 16 | context: Scope, 17 | FrequencyOptionsProperties {}: FrequencyOptionsProperties, 18 | ) -> View { 19 | let frequencies: &Signal> = create_signal(context, Vec::new()); 20 | 21 | if G::IS_BROWSER { 22 | spawn_local_scoped(context, async move { 23 | if let Some(data) = get_frequencies().await { 24 | frequencies.set(data); 25 | } 26 | }); 27 | } 28 | 29 | view! {context, 30 | select(name="frequency", class="form-select") { 31 | option(selected=true, disabled=true) { "Select the frequency" } 32 | Indexed( 33 | iterable=frequencies, 34 | view=|context, frequency: Frequency| { 35 | let display = frequency.to_string(); 36 | view!{ context, 37 | option(value=(frequency)) { (display) } 38 | } 39 | }, 40 | ) 41 | } 42 | } 43 | } 44 | 45 | pub async fn get_frequencies() -> Option> { 46 | let query_response = http_service::get_endpoint( 47 | format!("{}/{}", API_BASE_URL, API_FREQUENCY_LIST_ROUTE).as_str(), 48 | None, 49 | ) 50 | .await; 51 | 52 | match query_response { 53 | Some(response) => { 54 | let data: Vec = serde_json::from_str(&response).unwrap(); 55 | Some(data) 56 | } 57 | None => None, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/goal_list.rs: -------------------------------------------------------------------------------- 1 | use perseus::prelude::spawn_local_scoped; 2 | use sycamore::prelude::*; 3 | 4 | use crate::{ 5 | data::entity::goal::Goal, 6 | utility::{ 7 | constants::{API_BASE_URL, API_GOAL_LIST_ROUTE}, 8 | http_service, 9 | }, 10 | }; 11 | 12 | #[derive(Prop)] 13 | pub struct GoalListProperties<'a> { 14 | pub goals: &'a Signal>, 15 | } 16 | 17 | #[component] 18 | pub fn GoalList<'a, 'b: 'a, G: Html>( 19 | context: Scope<'a>, 20 | GoalListProperties { goals }: GoalListProperties<'b>, 21 | ) -> View { 22 | if G::IS_BROWSER { 23 | spawn_local_scoped(context, async move { 24 | if let Some(goal_list) = get_goals().await { 25 | goals.set(goal_list); 26 | } 27 | }); 28 | } 29 | view! { context, 30 | (if !goals.get().is_empty() { 31 | view! { context, 32 | ul(class=" goal_list", id="") { 33 | Keyed( 34 | iterable= goals, 35 | view = |context, goal: Goal| view!{ context, 36 | li() { (goal.name) } 37 | }, 38 | key= |goal| goal.id 39 | ) 40 | } 41 | } 42 | } else { 43 | view! { context, 44 | "No goals available." 45 | } 46 | }) 47 | } 48 | } 49 | 50 | pub async fn get_goals() -> Option> { 51 | let query_response = http_service::get_endpoint( 52 | format!("{}/{}", API_BASE_URL, API_GOAL_LIST_ROUTE).as_str(), 53 | None, 54 | ) 55 | .await; 56 | match query_response { 57 | Some(response) => { 58 | let goal_list_data: Vec = serde_json::from_str(&response).unwrap(); 59 | Some(goal_list_data) 60 | } 61 | None => None, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/form/input_validation.rs: -------------------------------------------------------------------------------- 1 | use sycamore::prelude::*; 2 | 3 | use crate::components::state::{ 4 | message_type::{get_message_type_icon, MessageType}, 5 | validation::Validation, 6 | visibility::Visibility, 7 | }; 8 | 9 | #[derive(Prop)] 10 | pub struct InputValidationProperties<'a> { 11 | pub content: &'a ReadSignal, 12 | pub visibility: &'a ReadSignal, 13 | pub validity: &'a ReadSignal, 14 | pub message_type: &'a ReadSignal, 15 | } 16 | 17 | #[component] 18 | pub fn InputValidation<'a, 'b: 'a, G: Html>( 19 | context: Scope<'a>, 20 | InputValidationProperties { 21 | content, 22 | visibility, 23 | validity, 24 | message_type, 25 | }: InputValidationProperties<'b>, 26 | ) -> View { 27 | view! {context, 28 | (match *visibility.get(){ 29 | Visibility::Visible => { 30 | view!{ context, 31 | div( 32 | class=(match *message_type.get() { 33 | MessageType::Information => "input-validation", 34 | MessageType::Success => "input-validation input-validation--success", 35 | MessageType::Error => "input-validation input-validation--error", 36 | MessageType::Warning => "input-validation input-validation--warning" 37 | }) 38 | ) { 39 | span( 40 | class="input-validation-icon", 41 | dangerously_set_inner_html=get_message_type_icon(&message_type.get()) 42 | ) { 43 | 44 | } 45 | output(class="input-validation-content") { (content.get()) } 46 | } 47 | } 48 | }, 49 | Visibility::Hidden => view!{context,}, 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /documentation/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | ## List 4 | 5 | - [ ] Accessability 6 | - [x] Chronicle Data Entity 7 | - [ ] Client Side Data Storage 8 | - [x] Client Side Rust via WebAssembly 9 | - [ ] Color Palette 10 | - [ ] Containerization 11 | - [ ] Continuous Integration 12 | - [ ] Continuous Delivery 13 | - [ ] Dark and Light Mode 14 | - [x] Database 15 | - [ ] Data Validation 16 | - [ ] Deployment Strategies 17 | - [ ] Design System 18 | - [x] Domain Name 19 | - [x] Email Address Data Entity 20 | - [ ] Email Address Validation 21 | - [x] Email Address User Identification 22 | - [ ] Encryption 23 | - [ ] HTTPS/TLS/SSL 24 | - [x] HTTP Authentication 25 | - [x] HTTP Cookies 26 | - [x] HTTP Registration 27 | - [ ] Integration Tests 28 | - [ ] Logging 29 | - [ ] NoSQL Database 30 | - [ ] Observability 31 | - [ ] OPAQUE 32 | - [ ] Optimization 33 | - [ ] Performance 34 | - [x] Person/User Data Entity 35 | - [x] Perseus 36 | - [x] PostgreSQL 37 | - [x] Password User Credential 38 | - [ ] Registration 39 | - [x] Rust 40 | - [ ] Security 41 | - [x] Server Side Data Storage 42 | - [ ] Sessions 43 | - [ ] Software Bill of Materials 44 | - [x] SQL Database 45 | - [ ] Stress Tests 46 | - [ ] Styling 47 | - [x] Sycamore 48 | - [ ] Testing 49 | - [ ] Themes 50 | - [ ] Unit Tests 51 | - [x] User Interface 52 | - [x] WebAssembly 53 | - [ ] WebAuthentication API User Credential 54 | - [ ] Website 55 | - [x] Web Server / HTTP Server 56 | 57 | ## Dependency Hierarchy 58 | 59 | Features are built upon one another, and therefore create a hierarchy of dependencies. 60 | Relationships can be many-to-many? At least one-to-many. 61 | There are concepts and implementations. 62 | Some implementations are completed by loremaster developers, others are not. 63 | 64 | ## Concepts 65 | 66 | - Computer 67 | - Server 68 | - Rust Language 69 | - HTTP Server 70 | - TLS 71 | - Logging 72 | - Client 73 | - HTML/CSS 74 | - WebAssembly 75 | - Rust Language 76 | 77 | ## Implementations 78 | 79 | - Axum - HTTP Server 80 | -------------------------------------------------------------------------------- /loremaster-web-interface/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "loremaster-web-interface" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Sean Myers "] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | futures-util = { version = "0.3.30" } 11 | gloo-timers = { version = "0.3.0", features = ["futures"] } 12 | js-sys = { version = "0.3.68" } 13 | log = { version = "0.4.20" } 14 | perseus = { version = "0.4.2", features = ["hydrate"] } 15 | reqwasm = { version = "0.5.0" } 16 | serde = { version = "1.0.196", features = ["derive"] } 17 | serde_json = { version = "1.0.113" } 18 | sycamore = { version = "0.8.2" } 19 | time = { version = "0.3.34", features = [ 20 | "macros", 21 | "serde", 22 | "std", 23 | "local-offset", 24 | "wasm-bindgen", 25 | "parsing", 26 | "formatting", 27 | ] } 28 | uuid = { version = "1.7.0", features = ["serde", "v4", "js"] } 29 | wasm-bindgen = { version = "0.2.91" } 30 | wasm-bindgen-futures = { version = "0.4.40" } 31 | webauthn-rs-proto = { version = "0.4.9", default-features = false, features = [ 32 | "wasm", 33 | ] } 34 | web-sys = { version = "0.3.67", features = [ 35 | "AttestationConveyancePreference", 36 | "AuthenticatorSelectionCriteria", 37 | "AuthenticatorAttachment", 38 | "CredentialsContainer", 39 | "CredentialCreationOptions", 40 | "Document", 41 | "Element", 42 | "Event", 43 | "EventTarget", 44 | "HtmlDialogElement", 45 | "HtmlDocument", 46 | "Navigator", 47 | "PublicKeyCredentialCreationOptions", 48 | "PublicKeyCredentialParameters", 49 | "PublicKeyCredentialType", 50 | "PublicKeyCredentialRpEntity", 51 | "PublicKeyCredentialUserEntity", 52 | "Storage", 53 | "UserVerificationRequirement", 54 | "Window", 55 | ] } 56 | 57 | 58 | [target.'cfg(engine)'.dependencies] 59 | tokio = { version = "1.36.0", features = ["macros", "rt", "rt-multi-thread"] } 60 | perseus-axum = { version = "=0.4.2", features = ["dflt-server"] } 61 | 62 | [target.'cfg(client)'.dependencies] 63 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/chronicle/create_chronicle.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::info; 3 | use sqlx::{query_as, PgPool}; 4 | use time::{Date, OffsetDateTime}; 5 | use uuid::Uuid; 6 | 7 | use crate::data::entity::chronicle::Chronicle; 8 | 9 | const CREATE_CHRONICLE_QUERY: &str = " 10 | INSERT INTO 11 | public.chronicle (person_id, date_recorded, creation_time) 12 | VALUES 13 | ($1, $2, $3) 14 | RETURNING 15 | id 16 | , person_id 17 | , date_recorded 18 | , notes 19 | , creation_time 20 | ;"; 21 | 22 | const CREATE_CHRONICLE_QUERY_WITH_ID: &str = " 23 | INSERT INTO 24 | public.chronicle (id, person_id, date_recorded, creation_time) 25 | VALUES 26 | ($1, $2, $3, $4) 27 | RETURNING 28 | id 29 | , person_id 30 | , date_recorded 31 | , notes 32 | , creation_time 33 | ;"; 34 | 35 | pub async fn create_chronicle_query( 36 | database_connection: &PgPool, 37 | chronicle_date: &Date, 38 | chronicle_time: &OffsetDateTime, 39 | person_id: &Uuid, 40 | chronicle_id: &Option, 41 | ) -> Result { 42 | info!("QUERY CALL: create_chronicle_query"); 43 | match chronicle_id { 44 | Some(id) => { 45 | let query_result: Chronicle = query_as::<_, Chronicle>(CREATE_CHRONICLE_QUERY_WITH_ID) 46 | .bind(id) 47 | .bind(person_id) 48 | .bind(chronicle_date) 49 | .bind(chronicle_time) 50 | .fetch_one(database_connection) 51 | .await?; 52 | 53 | Ok(query_result) 54 | } 55 | None => { 56 | let query_result: Chronicle = query_as::<_, Chronicle>(CREATE_CHRONICLE_QUERY) 57 | .bind(person_id) 58 | .bind(chronicle_date) 59 | .bind(chronicle_time) 60 | .fetch_one(database_connection) 61 | .await?; 62 | 63 | Ok(query_result) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/navigation/top_nav_bar.rs: -------------------------------------------------------------------------------- 1 | use perseus::reactor::Reactor; 2 | use sycamore::prelude::*; 3 | 4 | use crate::components::widget::date_time::DateTime; 5 | use crate::components::{ 6 | icon::{GIT_MERGE_SVG_HTML, GLOBE_SVG_HTML, SEARCH_SVG_HTML, USER_CIRCLE_SVH_HTML}, 7 | widget::theme_toggle::ThemeToggle, 8 | }; 9 | use crate::global_state::ApplicationStateRx; 10 | 11 | use super::{get_home_link, NavigationLink}; 12 | 13 | #[component] 14 | pub fn TopNavBar(context: Scope) -> View { 15 | let user_authentication = 16 | Reactor::::from_cx(context).get_global_state::(context); 17 | 18 | let nav_classes: &str = "top-nav"; 19 | 20 | let home_link: NavigationLink = get_home_link(); 21 | 22 | view! {context, 23 | nav(class=nav_classes) { 24 | div(class="loremaster-banner") { 25 | a(href=home_link.html_href, id=home_link.html_id, class="loremaster-banner-link") { (home_link.display_text)} 26 | } 27 | div(id="top-nav-version") { 28 | div(class="glass-blur") { 29 | span(dangerously_set_inner_html=GIT_MERGE_SVG_HTML) {} 30 | span() { "2023.7.7"} 31 | } 32 | } 33 | ThemeToggle() 34 | div() { 35 | button( 36 | dangerously_set_inner_html=SEARCH_SVG_HTML, 37 | title="Search" 38 | ) {} 39 | } 40 | div() { 41 | button( 42 | dangerously_set_inner_html=GLOBE_SVG_HTML, 43 | title="Glossary" 44 | ) {} 45 | } 46 | DateTime() 47 | div(id="top-nav-user-icon") { 48 | button( 49 | title="You" 50 | ) { 51 | span(dangerously_set_inner_html=USER_CIRCLE_SVH_HTML) { } 52 | span() { (user_authentication.authentication.user_alias.get()) } 53 | } 54 | 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/email_address/create_email_address.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use log::info; 4 | use sqlx::{query_as, PgPool}; 5 | use time::OffsetDateTime; 6 | use uuid::Uuid; 7 | 8 | use crate::data::entity::email_address::EmailAddress; 9 | 10 | const EXISTING_ADDRESS_QUERY: &str = " 11 | SELECT 12 | id 13 | , display 14 | , local_part 15 | , domain 16 | , validated 17 | , validation_date 18 | , creation_date 19 | FROM 20 | public.email_address 21 | WHERE 22 | email_address.display = $1 23 | LIMIT 24 | 1 25 | ;"; 26 | 27 | const QUERY: &str = " 28 | INSERT INTO 29 | public.email_address ( 30 | id 31 | , display 32 | , local_part 33 | , domain 34 | , creation_date 35 | ) 36 | VALUES 37 | ($1, $2, $3, $4, $5) 38 | RETURNING 39 | id 40 | , display 41 | , local_part 42 | , domain 43 | , validated 44 | , validation_date 45 | , creation_date 46 | ;"; 47 | 48 | pub async fn create_email_address_query( 49 | database_connection: &PgPool, 50 | email_address: &email_address::EmailAddress, 51 | ) -> Result { 52 | info!("QUERY CALL: create_person_query"); 53 | let new_id: Uuid = Uuid::new_v4(); 54 | let creation_date: OffsetDateTime = OffsetDateTime::now_utc(); 55 | 56 | let potential_address: Option = 57 | query_as::<_, EmailAddress>(EXISTING_ADDRESS_QUERY) 58 | .bind(email_address.as_str()) 59 | .fetch_optional(database_connection) 60 | .await?; 61 | 62 | if let Some(existing_email_address) = potential_address { 63 | return Ok(existing_email_address); 64 | } 65 | 66 | let query_result: EmailAddress = query_as::<_, EmailAddress>(QUERY) 67 | .bind(new_id) 68 | .bind(email_address.as_str()) 69 | .bind(email_address.local_part()) 70 | .bind(email_address.domain()) 71 | .bind(creation_date) 72 | .fetch_one(database_connection) 73 | .await?; 74 | 75 | Ok(query_result) 76 | } 77 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/data/list/value_list.rs: -------------------------------------------------------------------------------- 1 | // use sycamore::prelude::*; 2 | 3 | // use crate::{ 4 | // data::entity::goal::Goal, 5 | // utility::{ 6 | // constants::{API_BASE_URL, API_GOAL_LIST_ROUTE}, 7 | // http_service, 8 | // }, 9 | // }; 10 | 11 | // pub struct ValueListProperties { 12 | // pub values: Signal>, 13 | // } 14 | 15 | // #[component(ValueList)] 16 | // pub fn value_list(ValueListProperties { values }: ValueListProperties) -> View { 17 | // if G::IS_BROWSER { 18 | // perseus::spawn_local(cloned!((values) => async move { 19 | // if let Some(value_list) = get_values().await { 20 | // values.set(value_list); 21 | // } 22 | // })); 23 | // } 24 | // view! { 25 | // (if goals.get().len() > 0 { 26 | // view! { 27 | // ul(class="value_list", id="") { 28 | // Keyed( KeyedProps { 29 | // iterable: values.handle(), 30 | // template: move |value: Value| { 31 | // view!{ 32 | // li() { (value.name) } 33 | // } 34 | // }, 35 | // key: |value| value.id 36 | // }) 37 | // } 38 | // } 39 | // } else { 40 | // view! { 41 | // div() { 42 | // "No values available." 43 | // } 44 | // } 45 | // }) 46 | // } 47 | // } 48 | 49 | // pub async fn get_values() -> Option> { 50 | // let query_response = http_service::get_endpoint( 51 | // format!("{}/{}", API_BASE_URL, API_GOAL_LIST_ROUTE).as_str(), 52 | // None, 53 | // ) 54 | // .await; 55 | // match query_response { 56 | // Some(response) => { 57 | // let goal_list_data: Vec = serde_json::from_str(&response).unwrap(); 58 | // Some(goal_list_data) 59 | // } 60 | // None => None, 61 | // } 62 | // } 63 | -------------------------------------------------------------------------------- /loremaster-web-server/src/data/query/person.rs: -------------------------------------------------------------------------------- 1 | pub mod action_is_related; 2 | pub mod add_action; 3 | pub mod add_goal; 4 | pub mod add_password; 5 | pub mod add_web_authentication_key; 6 | pub mod alias_by_id; 7 | pub mod create_person; 8 | pub mod credential_by_email_address; 9 | pub mod get_person_sleep_schedule; 10 | pub mod goal_is_related; 11 | pub mod meta_by_id; 12 | pub mod person_by_email_address; 13 | pub mod remove_one_goal; 14 | pub mod update_email_address; 15 | pub mod update_meta_by_id; 16 | pub mod update_person_sleep_schedule; 17 | 18 | // #[cfg(test)] 19 | // mod tests { 20 | // use anyhow::Result; 21 | 22 | // use crate::{ 23 | // data::{ 24 | // entity::person::Person, postgres_handler::PostgresHandler, 25 | // query::person::create_person::create_person_query, 26 | // }, 27 | // utility::password_encryption::{PasswordEncryption, PasswordEncryptionService}, 28 | // }; 29 | 30 | // use super::person_by_email_address::_person_by_email_address_query; 31 | 32 | // const TEST_EMAIL: &str = "testemail@email.com"; 33 | // const TEST_PASSWORD: &str = "testPassword123!"; 34 | 35 | // #[tokio::test] 36 | // async fn test_create_person() -> Result<()> { 37 | // let postgres_context: PostgresHandler = PostgresHandler::new().await?; 38 | 39 | // let encrypted_password: String = 40 | // PasswordEncryptionService::encrypt_password(TEST_PASSWORD)?; 41 | 42 | // let _new_person: Person = create_person_query( 43 | // &postgres_context.database_pool, 44 | // &TEST_EMAIL.to_string(), 45 | // &encrypted_password, 46 | // None, 47 | // None, 48 | // ) 49 | // .await?; 50 | 51 | // return Ok(()); 52 | // } 53 | 54 | // #[tokio::test] 55 | // async fn test_person_by_email_address_query() -> Result<()> { 56 | // let postgres_context: PostgresHandler = PostgresHandler::new().await?; 57 | 58 | // let _query_result: Option = _person_by_email_address_query( 59 | // &postgres_context.database_pool, 60 | // &TEST_EMAIL.to_string(), 61 | // ) 62 | // .await?; 63 | 64 | // return Ok(()); 65 | // } 66 | // } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loremaster 2 | 3 | [![Web Server Build](https://github.com/seanpmyers/loremaster/actions/workflows/web_server_build.yml/badge.svg)](https://github.com/seanpmyers/loremaster/actions/workflows/web_server_build.yml) 4 | [![Web Interface Build](https://github.com/seanpmyers/loremaster/actions/workflows/web_interface_build.yaml/badge.svg)](https://github.com/seanpmyers/loremaster/actions/workflows/web_interface_build.yaml) 5 | [![rust-clippy analyze](https://github.com/seanpmyers/loremaster/workflows/rust-clippy%20analyze/badge.svg)](https://github.com/seanpmyers/loremaster/actions) 6 | 7 | Streamlined planning and tracking of life goals, actions, and outcomes. 8 | 9 | ## Intentions 10 | 11 | I have a desire to properly track how the actions I take everday work towards my desired objectives and convictions. 12 | 13 | I also want to be able to map out the mundane tasks of my life, and reduce the amount of mental energy I spend thinking about things that I need to do. 14 | 15 | The goal of this application is to attempt to achieve the above objectives, but I also generally enjoy building software and learning new technologies. 16 | 17 | List of intentions: 18 | 19 | - Copy all personally important information from ones head and store it into one organized location. 20 | - Improve the life of the user. 21 | - Improve the user's ability to plan for the future. 22 | - Reduce effort to organize and track past/future actions taken in life. 23 | - Reduce the need of ones brain to keep track of mundane information. 24 | - Reflect on how one's actions match their life convictions/objectives. 25 | 26 | ## Concept/Inspirations 27 | 28 | The idea for this application could be considered a form of/is inspired by the following concepts: 29 | 30 | - TODO lists 31 | - Quest logs in video games 32 | - Documentation 33 | - Journaling 34 | - Diaries 35 | - [personal knowledge managment (PKM)](https://en.wikipedia.org/wiki/Personal_knowledge_management) 36 | - [personal wiki](https://en.wikipedia.org/wiki/Personal_wiki) 37 | - [intelligence amplification](https://en.wikipedia.org/wiki/Intelligence_amplification) 38 | - [memex](https://en.wikipedia.org/wiki/Memex) 39 | - [bullet journal](https://en.wikipedia.org/wiki/Bullet_journal) 40 | - [commonplace book](https://en.wikipedia.org/wiki/Commonplace_book) 41 | -------------------------------------------------------------------------------- /documentation/entity/chronicle/feature/chronicled_actions.md: -------------------------------------------------------------------------------- 1 | # Chronicled Actions 2 | 3 | > Recording of actions that are taken or the intention of taking certain actions through chronicles. 4 | 5 | ## Requirements 6 | 7 | - [ ] database schema 8 | - [ ] intention 9 | - [x] action 10 | - [ ] Can record an intended action for today's chronicle 11 | - [ ] back-end 12 | - [ ] database 13 | - [ ] relation to intention 14 | - [ ] relation to action 15 | - [ ] relation to person 16 | - [ ] relation to chronicle 17 | - [ ] relation to completed action 18 | - [ ] optional timestamp 19 | - [ ] create, read, update, delete 20 | - [ ] handler 21 | - [ ] add handlers for create, read, update, delete 22 | - [ ] router 23 | - [ ] add routes for create, read, update, delete 24 | - [ ] front-end 25 | - [ ] create user interface component showing intentions 26 | - [ ] on chronicle page load, make API call to show list of intentions 27 | - [ ] list intentions with their meta-data 28 | - [ ] add button to delete individual intentions 29 | - [ ] add component to create new intention for the day 30 | - [ ] create save/add button 31 | - [ ] create action selection component 32 | - [ ] reload list component on addition 33 | - [ ] Can record an action taken for today's chronicle 34 | - [ ] back-end 35 | - [ ] database 36 | - [ ] add queries for create, read, update, delete 37 | - [ ] relation to action 38 | - [ ] relation to completed action 39 | - [ ] relation to person 40 | - [ ] relation to chronicle 41 | - [ ] optional timestamp 42 | - [ ] handler 43 | - [ ] add handlers for create, read, update, delete 44 | - [ ] router 45 | - [ ] add routes for create, read, update, delete 46 | - [ ] front-end 47 | - [ ] create user interface component showing intentions 48 | - [ ] on chronicle page load, make API call to show list of intentions 49 | - [ ] list intentions with their meta-data 50 | - [ ] add button to delete individual intentions 51 | - [ ] add component to create new intention for the day 52 | - [ ] create save/add button 53 | - [ ] create action selection component 54 | - [ ] reload list component on addition 55 | -------------------------------------------------------------------------------- /loremaster-web-server/src/api/handler/chronicle.rs: -------------------------------------------------------------------------------- 1 | use crate::data::{ 2 | entity::{ 3 | chronicle::{current_server_time, get_date_from_timezone, Chronicle}, 4 | transfer::person_chronicle::PersonChronicle, 5 | }, 6 | query::{ 7 | self, 8 | chronicle::{ 9 | create_chronicle::create_chronicle_query, 10 | current_chronicle_by_person::get_current_chronicle_by_person_query, 11 | }, 12 | }, 13 | }; 14 | use anyhow::{anyhow, Result}; 15 | use log::info; 16 | use sqlx::{Pool, Postgres}; 17 | use time::OffsetDateTime; 18 | use uuid::Uuid; 19 | 20 | pub async fn handle_get_today( 21 | database_pool: &Pool, 22 | person_id: &Uuid, 23 | requested_timezone_string: &Option, 24 | ) -> Result { 25 | let requested_date: OffsetDateTime = match requested_timezone_string { 26 | Some(input) => { 27 | let sanitized_timezone_string: &str = input.trim(); 28 | get_date_from_timezone(current_server_time()?, sanitized_timezone_string)? 29 | } 30 | None => current_server_time()?, 31 | }; 32 | 33 | let person_alias: Option = 34 | query::person::alias_by_id::alias_by_id_query(database_pool, person_id) 35 | .await 36 | .map_err(|error| anyhow!("{}", error))?; 37 | 38 | let chronicle_query_result: Option = 39 | get_current_chronicle_by_person_query(database_pool, &requested_date, person_id) 40 | .await 41 | .map_err(|error| anyhow!("{}", error))?; 42 | 43 | let chronicle: Chronicle = match chronicle_query_result { 44 | Some(existing_chronicle) => existing_chronicle, 45 | None => { 46 | info!("No chronicle exits for the current date. Creating one."); 47 | let new_chronicle_id: Uuid = Uuid::new_v4(); 48 | create_chronicle_query( 49 | database_pool, 50 | &requested_date.date(), 51 | &requested_date, 52 | person_id, 53 | &Some(new_chronicle_id), 54 | ) 55 | .await 56 | .map_err(|error| anyhow!("{}", error))? 57 | } 58 | }; 59 | 60 | Ok(PersonChronicle { 61 | chronicle_id: chronicle.id, 62 | chronicle_date: chronicle.date_recorded, 63 | person_alias, 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /loremaster-web-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "loremaster-web-server" 3 | version = "0.1.0" 4 | authors = ["Sean Myers "] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | # Error handling 11 | anyhow = "1.0.79" 12 | # Password Encryption 13 | argon2 = "0.5.0" 14 | # A web application framework that focuses on ergonomics and modularity. 15 | axum = { version = "0.6.20", features = ["headers", "http2", "ws", "tokio"] } 16 | axum-extra = { version = "0.8.0", features = [ 17 | "cookie", 18 | "cookie-private", 19 | "cookie-signed", 20 | "form", 21 | ] } 22 | axum-server = { version = "0.5.1", features = ["tls-rustls"] } 23 | # Fast IDentity Online client to authenticator protocol (FIDO2 CTAP) 24 | # ctap-hid-fido2 = { version = "3.4.2" } 25 | # Email adderss validation 26 | email_address = { version = "0.2.4" } 27 | # Logging 28 | env_logger = "0.11.1" 29 | # Provides async programming foundational functionality 30 | futures = "0.3.30" 31 | # Logging 32 | log = { version = "0.4.20" } 33 | # Rust Object Notation 34 | ron = { version = "0.8.1" } 35 | # Random nubmer generation 36 | rand = "0.8.5" 37 | # Serialization 38 | serde = { version = "1.0.196", features = ["derive"] } 39 | serde_json = "1.0.113" 40 | # Database client/pool/toolkit 41 | sqlx = { version = "0.6.3", features = [ 42 | "json", 43 | "macros", 44 | "migrate", 45 | # "offline", 46 | "postgres", 47 | "runtime-tokio-native-tls", 48 | "time", 49 | "tls", 50 | "uuid", 51 | ] } 52 | # NewSQL database -- to be used as an alternative to postgresql for this app 53 | # surrealdb = { version = "1.0.0-beta.8" } 54 | # Error/exception handling 55 | thiserror = "1.0.56" 56 | # Time 57 | time = { version = "0.3.34", features = ["serde", "std", "parsing"] } 58 | time-tz = { version = "1.0.3" } 59 | # Backend async I/O functionality 60 | tokio = { version = "1.36.0", features = ["full"] } 61 | tokio-stream = { version = "0.1.14" } 62 | tokio-test = { version = "0.4.3" } 63 | # Tower middleware and utilities for HTTP clients and servers 64 | tower-http = { version = "0.4.4", features = ["cors", "fs"] } 65 | # Implementation of Universally Unique Identifiers (Uuid) 66 | uuid = { version = "1.7.0", features = ["v4", "serde"] } 67 | webauthn-rs = { version = "0.4.8", features = [ 68 | "danger-allow-state-serialisation", 69 | ] } 70 | -------------------------------------------------------------------------------- /documentation/implementation/password_encryption.md: -------------------------------------------------------------------------------- 1 | # Password Encryption 2 | 3 | ## Why 4 | 5 | TODO: 6 | 7 | ## How 8 | 9 | For secure password storage [argon2](https://en.wikipedia.org/wiki/Argon2) will be used for encryption before storing user passwords in the database. 10 | 11 | Flow looks something like this: 12 | 13 | 1. Configuration is defined and stored in a secret file somewhere on the server. This file should not be source controlled. 14 | - The configuration requires a few fields: 15 | - Sitewide secret 16 | - This is a string generated from a secure psuedo random number generator 17 | - Use argon2 to hash this with user specific salt to create the salt used in argon2 hash of user password. This makes it so that if your database is compromised the attacker must also have access to local files to crack passwords from the database. 18 | - Number of iterations for argon2 19 | - This is an implementation of a concept called Key Strengthening/Stretching 20 | - If it takes 0.00001 seconds for your hash function to return, someone can attempt to brute forcing 100,000 passwords a second until they find a match. But if it takes 1 second for your hash function to spit out a result, it means brute forcing therefore takes 100,000 times longer to test each case. 21 | - In order to slow down the hashing function, we run the argon2 hash however many times specified in this field. 22 | 2. Password hash request is recieved and computed for new user 23 | - The iteration and sitewide secret are read from the configuration file 24 | - The password is hashed using the formula: hash(hash(user_salt + site_secret) + password) 25 | - A new user specific salt is generated by a secure psuedo random number generator 26 | - The user specific salt and the sitewide hash are hashed together using argon2 to create the password salt 27 | - The user's password is hashed with the password salt to create the hashed password 28 | 3. The hashed password is stored with the iterations, and user specific salt in a string and added to the database in the following string format: "iterations.user_salt.hashed_key" 29 | 4. For verification of a user password the fields are split out from the database string and used to created a new hash using the user input. The out put byte array is then compared with the hashed key from the database to determine if access should be granted. 30 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/calendar/week.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Add; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use sycamore::prelude::*; 5 | 6 | use crate::utility::constants::DAYS_OF_WEEK; 7 | 8 | #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] 9 | pub struct WeekDayInformation { 10 | pub number: u8, 11 | pub week_day: time::Weekday, 12 | } 13 | 14 | #[derive(Prop)] 15 | pub struct WeekProperties<'a> { 16 | pub selected_date: &'a Signal, 17 | pub days: &'a Signal>, 18 | } 19 | 20 | #[component] 21 | pub fn Week<'a, 'b: 'a, G: Html>( 22 | context: Scope<'a>, 23 | WeekProperties { 24 | selected_date, 25 | days, 26 | }: WeekProperties<'b>, 27 | ) -> View { 28 | days.set(create_week_list(&selected_date.get())); 29 | view! {context, 30 | div(class="week-widget", id="") { 31 | Keyed( 32 | iterable= days, 33 | view= |context, day: WeekDayInformation| 34 | { 35 | let mut day_div_classes = String::from("card"); 36 | if day.number == selected_date.get().day() { day_div_classes.push_str(" active-card text-light") ;} 37 | else { day_div_classes.push_str(" bg-white")} 38 | view!{ context, 39 | div(class=(day_div_classes)) { 40 | div(class="m-1") { 41 | (day.number) 42 | } 43 | div(class="m-1") { 44 | (day.week_day.to_string()) 45 | } 46 | } 47 | }}, 48 | key= |day| day.number 49 | ) 50 | } 51 | } 52 | } 53 | 54 | pub fn create_week_list(selected_date: &time::OffsetDateTime) -> Vec { 55 | let selected_weekday: time::Weekday = selected_date.weekday(); 56 | let mut result: Vec = vec![]; 57 | 58 | for (index, day) in DAYS_OF_WEEK.iter().enumerate() { 59 | let number: u8 = selected_date 60 | .add(time::Duration::days( 61 | -(selected_weekday.number_days_from_sunday() as i64), 62 | )) 63 | .add(time::Duration::days(index as i64)) 64 | .day(); 65 | 66 | result.push(WeekDayInformation { 67 | number, 68 | week_day: *day, 69 | }) 70 | } 71 | 72 | result 73 | } 74 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/main.rs: -------------------------------------------------------------------------------- 1 | use perseus::prelude::{Html, PerseusApp, PerseusRoot}; 2 | 3 | pub mod components; 4 | pub mod error_pages; 5 | pub mod data; 6 | pub mod templates; 7 | pub mod utility; 8 | pub mod global_state; 9 | 10 | 11 | #[perseus::main(perseus_axum::dflt_server)] 12 | pub fn main() -> PerseusApp { 13 | PerseusApp::new() 14 | .template(crate::templates::index::get_template()) 15 | .template(crate::templates::about::get_template()) 16 | .template(crate::templates::login::get_template()) 17 | .template(crate::templates::chronicle::get_template()) 18 | .template(crate::templates::registration::get_template()) 19 | .template(crate::templates::you::get_template()) 20 | .template(crate::templates::timeline::get_template()) 21 | .template(crate::templates::design_system::get_template()) 22 | .error_views(crate::error_pages::get_error_pages()) 23 | .index_view(|context| { 24 | sycamore::view! { context, 25 | // We don't need a ``, that's added automatically by Perseus (though that can be overriden if you really want by using `.index_view_str()`) 26 | // We need a `` and a `` at the absolute minimum for Perseus to work properly (otherwise certain script injections will fail) 27 | link(rel="icon", type="image/x-icon", href="/.perseus/static/favicon_io/favicon.ico") {} 28 | link(rel="stylesheet", href="/.perseus/static/styles/loremaster/index.css"){} 29 | link(rel="stylesheet", href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap") 30 | head { 31 | 32 | } 33 | body() { 34 | // This creates an element into which our app will be interpolated 35 | // This uses a few tricks internally beyond the classic `
`, so we use this wrapper for convenience 36 | PerseusRoot() 37 | // Note that elements in here can't be selectively removed from one page, it's all-or-nothing in the index view (it wraps your whole app) 38 | // Note also that this won't be reloaded, even when the user switches pages 39 | } 40 | } 41 | }) 42 | .global_state_creator(crate::global_state::get_global_state_creator()) 43 | } 44 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/utility/constants.rs: -------------------------------------------------------------------------------- 1 | pub const API_BASE_URL: &str = "https://localhost:8000"; 2 | pub const API_REGISTER_URL: &str = "https://localhost:8000/authentication/register"; 3 | pub const API_LOGIN_URL: &str = "https://localhost:8000/authentication/authenticate"; 4 | pub const API_CHRONICLE_TODAY_URL: &str = "https://localhost:8000/chronicle/today"; 5 | pub const API_PERSON_META_DATA_ROUTE: &str = "person/meta"; 6 | pub const API_PERSON_META_UPDATE_ROUTE: &str = "person/update/meta"; 7 | pub const API_PERSON_SLEEP_SCHEDULE_UPDATE_ROUTE: &str = "person/update/sleep-schedule"; 8 | pub const API_PERSON_SLEEP_SCHEDULE_ROUTE: &str = "person/sleep-schedule"; 9 | pub const API_PERSON_EMAIL_ADDRESS_UPDATE_ROUTE: &str = "person/update/email_address"; 10 | pub const API_ACTION_NEW_ROUTE: &str = "person/action-new"; 11 | pub const API_ACTION_LIST_ROUTE: &str = "person/action-list"; 12 | pub const API_FREQUENCY_LIST_ROUTE: &str = "person/frequency-list"; 13 | pub const API_LOGOUT_ROUTE: &str = "authentication/logout"; 14 | pub const API_GOAL_NEW_ROUTE: &str = "person/goal-new"; 15 | pub const API_GOAL_LIST_ROUTE: &str = "person/goal-list"; 16 | pub const API_WEBAUTHN_START_ROUTE: &str = "authentication/webauthn/start"; 17 | pub const API_WEBAUTHN_FINISH_ROUTE: &str = "authentication/webauthn/finish"; 18 | pub const API_WEBAUTHN_LOGIN_START_ROUTE: &str = "authentication/webauthn/login/start"; 19 | pub const API_WEBAUTHN_LOGIN_END_ROUTE: &str = 20 | "authentication/webauthn/login/finish/person_public_key_credential.json"; 21 | pub const HTTP_HEADER_CONTENT_TYPE: &str = "Content-Type"; 22 | pub const HTTP_HEADER_CONTENT_TYPE_FORM: &str = "application/x-www-form-urlencoded"; 23 | pub const EMAIL_ADDRESS_FIELD: &str = "email_address"; 24 | pub const PASSWORD_FIELD: &str = "password"; 25 | 26 | pub const OK_HTTP_STATUS_CODE: u16 = 200; 27 | pub const ACCEPTED_HTTP_STATUS_CODE: u16 = 202; 28 | 29 | pub const JANUARY: &str = "January"; 30 | pub const FEBRUARY: &str = "February"; 31 | pub const MARCH: &str = "March"; 32 | pub const APRIL: &str = "April"; 33 | pub const MAY: &str = "May"; 34 | pub const JUNE: &str = "June"; 35 | pub const JULY: &str = "July"; 36 | pub const AUGUST: &str = "August"; 37 | pub const SEPTEMBER: &str = "September"; 38 | pub const OCTOBER: &str = "October"; 39 | pub const NOVEMBER: &str = "November"; 40 | pub const DECEMBER: &str = "December"; 41 | 42 | pub const DAYS_OF_WEEK: &[time::Weekday; 7] = &[ 43 | time::Weekday::Sunday, 44 | time::Weekday::Monday, 45 | time::Weekday::Tuesday, 46 | time::Weekday::Wednesday, 47 | time::Weekday::Thursday, 48 | time::Weekday::Friday, 49 | time::Weekday::Saturday, 50 | ]; 51 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/accordion.rs: -------------------------------------------------------------------------------- 1 | use perseus::prelude::spawn_local_scoped; 2 | use sycamore::prelude::*; 3 | use web_sys::Event; 4 | 5 | use crate::components::icon::{CHEVRON_DOWN_SVG_HTML, CHEVRON_UP_SVG_HTML}; 6 | 7 | #[derive(Prop)] 8 | pub struct AccordionProperties<'a, G: Html> { 9 | pub id: &'a ReadSignal, 10 | pub children: Children<'a, G>, 11 | } 12 | 13 | #[component] 14 | pub fn Accordion<'a, G: Html>( 15 | context: Scope<'a>, 16 | AccordionProperties { id, children }: AccordionProperties<'a, G>, 17 | ) -> View { 18 | let children = children.call(context); 19 | view! {context, 20 | div( 21 | id=id.get(), 22 | class="accordion" 23 | ) { (children) } 24 | } 25 | } 26 | 27 | #[derive(Prop)] 28 | pub struct AccordionItemProperties<'a, G: Html> { 29 | pub title: &'a Signal, 30 | pub children: Children<'a, G>, 31 | } 32 | 33 | #[component] 34 | pub fn AccordionItem<'a, G: Html>( 35 | context: Scope<'a>, 36 | AccordionItemProperties { title, children }: AccordionItemProperties<'a, G>, 37 | ) -> View { 38 | let children = children.call(context); 39 | let collapsed: &Signal = create_signal(context, true); 40 | let item_css_classes: &Signal<&str> = create_signal(context, "accordion-collapse collapse"); 41 | let svg: &Signal<&str> = create_signal(context, CHEVRON_UP_SVG_HTML); 42 | let handle_click = move |event: Event| { 43 | event.prevent_default(); 44 | if G::IS_BROWSER { 45 | spawn_local_scoped(context, async move { 46 | collapsed.set(!*collapsed.get()); 47 | match *collapsed.get() { 48 | true => { 49 | item_css_classes.set("accordion-collapse collapse"); 50 | svg.set(CHEVRON_UP_SVG_HTML); 51 | } 52 | false => { 53 | item_css_classes.set("accordion-collapse collapse show"); 54 | svg.set(CHEVRON_DOWN_SVG_HTML); 55 | } 56 | } 57 | }); 58 | } 59 | }; 60 | 61 | view! {context, 62 | div(class="accordion-item") { 63 | h2(class="accordion-item-title") { 64 | button(on:click=handle_click, class="accordion-toggle") { 65 | span() { (title.get()) } 66 | span(class="accordion-shevron", dangerously_set_inner_html=*svg.get()) { } 67 | } 68 | } 69 | div(class=item_css_classes) { 70 | div(class="accordion-item-body") { 71 | (children) 72 | } 73 | } 74 | } 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /documentation/features/action_planning.md: -------------------------------------------------------------------------------- 1 | # Action Planning 2 | 3 | - [`chronicle`] 4 | - [`action`] 5 | - [`feature`] 6 | 7 | Chronicles represent all the lore associated with a given date in time. 8 | 9 | Being able to track and plan actions/tasks/routines is one of the most important features for me currently. 10 | 11 | I think actions is still a good encompassing word for the concept I am trying to capture. 12 | 13 | Actions have multiple dimensions to them. 14 | 15 | Examples: 16 | 17 | - `duration` 18 | - actions can occur over a period of time or in an instant 19 | - `frequency` 20 | - actions can be taken yearly, monthly, daily, hourly, etc. 21 | - could be every Thursday and Sunday 22 | - could be the first day of every quarter of the year 23 | - could be a desired number of completions within a repeated time-frame 24 | - could use a time range since the last completion time 25 | - `procedure` 26 | - a series of actions completed in a certain order or manner 27 | - `routine` 28 | - a collection of actions taken on a recurring basis 29 | - `contingent actions` 30 | - occurring or existing only if (certain circumstances) are the case; dependent on something else 31 | - `outcome` 32 | - what is the result of an action taken, if there is one 33 | - positive/negatives -- pros/cons -- risk/reward 34 | - anticipated vs actual outcome (pros/cons) 35 | - confidence level in outcome before taking the action 36 | - `category` 37 | - human actions tend to fall under general categories 38 | - wellbeing/rest 39 | - sleeping 40 | - napping 41 | - meditation 42 | - spa 43 | - health/bodily maintenance 44 | - strength training 45 | - cardio training 46 | - stretching 47 | - organization/inventory upkeep 48 | - keeping inventory 49 | - entertainment/enjoyment 50 | - gaming 51 | - sports 52 | - movies 53 | - social media 54 | - food/beverage 55 | - experiential/observational 56 | - bird watching 57 | - hiking 58 | - exploring 59 | - touring 60 | - traveling 61 | - skill work 62 | - programming 63 | - design 64 | - painting 65 | - drawing 66 | - sports 67 | - woodwork 68 | - learning 69 | - studying 70 | - lectures 71 | - school 72 | - educational content 73 | - books 74 | - videos 75 | - social 76 | - party 77 | - events 78 | - gatherings 79 | - holidays 80 | - financial 81 | - paying taxes 82 | - work 83 | - side-hustle 84 | - investing 85 | - real estate 86 | - can fall under more than one 87 | 88 | In order to fully implement `action planning`, I think most of these concepts ought to be included. 89 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/widget/date_time.rs: -------------------------------------------------------------------------------- 1 | use futures_util::{future::ready, stream::StreamExt}; 2 | use gloo_timers::future::IntervalStream; 3 | use js_sys::{Array, Object}; 4 | use perseus::prelude::spawn_local_scoped; 5 | use sycamore::prelude::*; 6 | use time::{macros::format_description, OffsetDateTime}; 7 | use wasm_bindgen::JsValue; 8 | 9 | const ONE_SECOND_IN_MILLISECONDS: u32 = 1_000; 10 | 11 | #[component] 12 | pub fn DateTime(context: Scope) -> View { 13 | let short_date: &Signal = create_signal(context, String::new()); 14 | let day_month: &Signal = create_signal(context, String::new()); 15 | let time: &Signal = create_signal(context, String::new()); 16 | let time_zone: &Signal = create_signal(context, String::new()); 17 | 18 | if G::IS_BROWSER { 19 | let short_format = format_description!( 20 | "[year]/[month]/[day] [hour repr:12 padding:space]:[minute]:[second] [period]" 21 | ); 22 | let time_format = format_description!("[hour repr:12 padding:space]:[minute] [period]"); 23 | let rust_time: OffsetDateTime = time::OffsetDateTime::now_local().unwrap(); 24 | short_date.set(rust_time.format(short_format).unwrap()); 25 | time.set(rust_time.format(time_format).unwrap()); 26 | day_month.set(format!( 27 | "{}, {} {}", 28 | rust_time.weekday(), 29 | rust_time.month(), 30 | rust_time.date().day() 31 | )); 32 | spawn_local_scoped(context, async move { 33 | let options = 34 | js_sys::Intl::DateTimeFormat::new(&Array::new(), &Object::new()).resolved_options(); 35 | time_zone.set( 36 | js_sys::Reflect::get(&options, &JsValue::from("timeZone")) 37 | .unwrap() 38 | .as_string() 39 | .unwrap(), 40 | ); 41 | 42 | IntervalStream::new(ONE_SECOND_IN_MILLISECONDS) 43 | .for_each(|_| { 44 | let rust_time = time::OffsetDateTime::now_local().unwrap(); 45 | short_date.set(rust_time.format(short_format).unwrap()); 46 | time.set(rust_time.format(time_format).unwrap()); 47 | day_month.set(format!( 48 | "{}, {} {}", 49 | rust_time.weekday(), 50 | rust_time.month(), 51 | rust_time.date().day() 52 | )); 53 | ready(()) 54 | }) 55 | .await; 56 | }); 57 | } 58 | let widget_classes = "date-time-widget"; 59 | view! {context, 60 | section(class=widget_classes) { 61 | div() { (day_month.get()) } 62 | div() { (short_date.get()) } 63 | div() { (time_zone.get()) } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/navigation.rs: -------------------------------------------------------------------------------- 1 | use super::icon::{ 2 | SvgIcon, ARCHIVE_SVG_HTML, BOOK_SVG_HTML, CLOCK_SVG_HTML, FEATHER_SVG_HTML, 3 | HELP_CIRCLE_SVG_HTML, LAYOUT_SVG_HTML, LOGIN_SVG_HTML, USER_PLUS_SVG_HTML, USER_SVG_HTML, 4 | }; 5 | 6 | pub mod side_nav_bar; 7 | pub mod tab; 8 | pub mod top_nav_bar; 9 | 10 | #[derive(Debug, Clone, PartialEq, Eq)] 11 | pub struct NavigationLink<'a> { 12 | pub html_id: &'a str, 13 | pub html_href: &'a str, 14 | pub display_text: &'a str, 15 | pub svg_html: SvgIcon, 16 | } 17 | 18 | pub fn get_home_link() -> NavigationLink<'static> { 19 | NavigationLink { 20 | html_id: "index-link", 21 | html_href: "/", 22 | display_text: "Loremaster", 23 | svg_html: "", 24 | } 25 | } 26 | 27 | pub fn get_navigation_links() -> Vec> { 28 | vec![ 29 | NavigationLink { 30 | html_id: "you-link", 31 | html_href: "/you/", 32 | display_text: "You", 33 | svg_html: USER_SVG_HTML, 34 | }, 35 | NavigationLink { 36 | html_id: "chronicle-link", 37 | html_href: "/chronicle/", 38 | display_text: "Chronicle", 39 | svg_html: FEATHER_SVG_HTML, 40 | }, 41 | NavigationLink { 42 | html_id: "lore-link", 43 | html_href: "/lore/", 44 | display_text: "Lore", 45 | svg_html: BOOK_SVG_HTML, 46 | }, 47 | NavigationLink { 48 | html_id: "timeline-link", 49 | html_href: "/timeline/", 50 | display_text: "Timeline", 51 | svg_html: CLOCK_SVG_HTML, 52 | }, 53 | NavigationLink { 54 | html_id: "ownership-link", 55 | html_href: "/ownership/", 56 | display_text: "Ownership", 57 | svg_html: ARCHIVE_SVG_HTML, 58 | }, 59 | NavigationLink { 60 | html_id: "registration-link", 61 | html_href: "/registration/", 62 | display_text: "Registration", 63 | svg_html: USER_PLUS_SVG_HTML, 64 | }, 65 | NavigationLink { 66 | html_id: "login-link", 67 | html_href: "/login/", 68 | display_text: "Login", 69 | svg_html: LOGIN_SVG_HTML, 70 | }, 71 | NavigationLink { 72 | html_id: "about-link", 73 | html_href: "/about/", 74 | display_text: "About", 75 | svg_html: HELP_CIRCLE_SVG_HTML, 76 | }, 77 | ] 78 | } 79 | 80 | pub fn get_navigation_link_by_name(name: String) -> Option> { 81 | get_navigation_links() 82 | .iter() 83 | .find(|link| link.display_text == name) 84 | .map(|link| link.to_owned()) 85 | } 86 | 87 | pub const DESIGN_SYSTEM_LINK: NavigationLink = NavigationLink { 88 | html_id: "design-system-link", 89 | html_href: "/design-system/", 90 | display_text: "Design System", 91 | svg_html: LAYOUT_SVG_HTML, 92 | }; 93 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/combobox.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use sycamore::prelude::*; 3 | use uuid::Uuid; 4 | use web_sys::Event; 5 | 6 | pub const COMBOBOX_OPTION_CSS_CLASSES: &str = ""; 7 | pub const COMBOBOX_QUERY_INPUT_CSS_CLASSES: &str = ""; 8 | 9 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 10 | pub struct ComboBoxOption { 11 | pub id: Uuid, 12 | pub display_text: String, 13 | pub description: String, 14 | } 15 | 16 | #[derive(Prop)] 17 | pub struct ComboBoxProperties<'combobox> { 18 | pub classes: &'combobox ReadSignal, 19 | pub label: String, 20 | pub options: Vec, 21 | pub query: &'combobox Signal, 22 | pub selected: &'combobox Signal>, 23 | pub selected_html_input_name: String, 24 | } 25 | 26 | #[component] 27 | pub fn ComboBox<'combobox, G: Html>( 28 | context: Scope<'combobox>, 29 | ComboBoxProperties { 30 | classes, 31 | label, 32 | selected_html_input_name, 33 | options, 34 | query, 35 | selected, 36 | }: ComboBoxProperties<'combobox>, 37 | ) -> View { 38 | let filtered_options: &Signal> = create_signal(context, options.clone()); 39 | 40 | create_effect(context, move || { 41 | let query: String = query.get().trim().to_lowercase(); 42 | filtered_options.modify().clear(); 43 | filtered_options.set( 44 | options 45 | .iter() 46 | .cloned() 47 | .filter(|option| option.display_text.to_lowercase().contains(&query)) 48 | .collect::>(), 49 | ); 50 | filtered_options.modify().sort(); 51 | }); 52 | 53 | view! {context, 54 | label() { (label) } 55 | div(class=format!("{} combobox", classes)) { 56 | input(type="text", bind:value=query, class="combobox-input") {} 57 | input(type="hidden", value=(match selected.get().as_ref() { 58 | Some(option) => option.to_string(), 59 | None => String::new(), 60 | }), name=selected_html_input_name) 61 | ul(class="combobox-options ", role="listbox", aria-label=label) { 62 | Keyed( 63 | iterable=filtered_options, 64 | view= move |context, option| { 65 | let display_text = option.display_text.clone(); 66 | view! { context, 67 | li( 68 | class=COMBOBOX_OPTION_CSS_CLASSES, 69 | value=option.id.to_string(), 70 | title=option.description 71 | ) { button(on:click=move |event: Event| { 72 | event.prevent_default(); 73 | selected.set(Some(option.id)); 74 | query.set(display_text.to_owned()); 75 | }) { (option.display_text) } } 76 | } 77 | }, 78 | key=|option| option.id 79 | ) 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /loremaster-web-interface/src/components/navigation/side_nav_bar.rs: -------------------------------------------------------------------------------- 1 | use perseus::reactor::Reactor; 2 | use sycamore::prelude::*; 3 | 4 | use crate::{components::navigation::DESIGN_SYSTEM_LINK, global_state::ApplicationStateRx}; 5 | 6 | use super::{get_navigation_links, NavigationLink}; 7 | 8 | const NAV_CLASSES: &str = "side-nav"; 9 | const NAV_UL_CLASSES: &str = "side-nav-container"; 10 | const NAV_LI_CLASSES: &str = "side-nav-item"; 11 | const A_CLASS: &str = "big-nav-button side-nav-link"; 12 | const NAV_BUTTON_ICON_CLASS: &str = "big-nav-button-icon"; 13 | const NAV_BUTTON_TEXT_CLASS: &str = "big-nav-button-text"; 14 | 15 | #[component] 16 | pub fn SideNavBar(context: Scope) -> View { 17 | let user_authentication = 18 | Reactor::::from_cx(context).get_global_state::(context); 19 | let links: &Signal> = create_signal(context, get_navigation_links()); 20 | 21 | view! {context, 22 | nav(class=NAV_CLASSES) { 23 | ul(class=NAV_UL_CLASSES) { 24 | Indexed( 25 | iterable=links, 26 | view= move |context, link| 27 | { 28 | if link.html_href.eq("/you/") { 29 | return view! { context, 30 | li(class=NAV_LI_CLASSES) { 31 | a( 32 | class=A_CLASS, 33 | id=link.html_id, 34 | href=link.html_href 35 | ) { 36 | span(class=NAV_BUTTON_ICON_CLASS,dangerously_set_inner_html=link.svg_html) {} 37 | span(class=NAV_BUTTON_TEXT_CLASS) {(user_authentication.authentication.user_alias.get())} 38 | 39 | } 40 | } 41 | }; 42 | } 43 | view! { context, 44 | li(class=NAV_LI_CLASSES) { 45 | a( 46 | class=A_CLASS, 47 | id=link.html_id, 48 | href=link.html_href 49 | ) { 50 | span(class=NAV_BUTTON_ICON_CLASS,dangerously_set_inner_html=link.svg_html) {} 51 | span(class=NAV_BUTTON_TEXT_CLASS) {(link.display_text)} 52 | 53 | } 54 | } 55 | }} 56 | ) 57 | DesignSystemLink() 58 | } 59 | } 60 | } 61 | } 62 | 63 | #[component] 64 | pub fn DesignSystemLink(context: Scope) -> View { 65 | view! { context, 66 | li(class=NAV_LI_CLASSES) { 67 | a( 68 | class=A_CLASS, 69 | id=DESIGN_SYSTEM_LINK.html_id, 70 | href=DESIGN_SYSTEM_LINK.html_href 71 | ) { 72 | span(class=NAV_BUTTON_ICON_CLASS,dangerously_set_inner_html=DESIGN_SYSTEM_LINK.svg_html) {} 73 | span(class=NAV_BUTTON_TEXT_CLASS) {(DESIGN_SYSTEM_LINK.display_text)} 74 | 75 | } 76 | } 77 | } 78 | } 79 | --------------------------------------------------------------------------------