├── .env ├── .gitignore ├── .gitignore copy ├── Cargo.toml ├── README.md ├── crates ├── compile_api_macros │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── dal-tx-impl │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── event-subscriber │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── publish-event │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs └── utils │ ├── Cargo.toml │ └── src │ ├── compile_api.rs │ ├── config.rs │ ├── errors.rs │ ├── lib.rs │ └── test_api_endpoint.rs ├── dal ├── dal │ ├── Cargo.toml │ ├── migrations │ │ └── 20240523088625_initial-setup.sql │ └── src │ │ ├── connections │ │ ├── mod.rs │ │ └── sqlx_postgres.rs │ │ ├── define_transactions.rs │ │ ├── lib.rs │ │ ├── migrations.rs │ │ ├── rate_limit_entries │ │ ├── mod.rs │ │ ├── postgres_txs.rs │ │ └── tx_definitions.rs │ │ ├── role_permissions │ │ ├── mod.rs │ │ ├── postgres_tsx.rs │ │ └── tx_definitions.rs │ │ ├── to_do_items │ │ ├── mod.rs │ │ ├── postgres_tsx.rs │ │ └── tx_definitions.rs │ │ └── users │ │ ├── mod.rs │ │ ├── postgres_txs.rs │ │ └── tx_definitions.rs └── kernel │ ├── Cargo.toml │ └── src │ ├── email_invites.rs │ ├── lib.rs │ ├── rate_limit_entries.rs │ ├── role_permissions.rs │ ├── to_do_items.rs │ ├── token │ ├── checks.rs │ ├── mod.rs │ ├── session_cache │ │ ├── engine_mem.rs │ │ ├── engine_mock.rs │ │ ├── mod.rs │ │ ├── structs.rs │ │ └── traits.rs │ └── token.rs │ └── users.rs ├── docker-compose.yml ├── frontends ├── apiModules │ ├── auth │ │ ├── index.ts │ │ └── localStorage.ts │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ ├── serverApi │ │ ├── auth │ │ │ ├── auth │ │ │ │ ├── index.ts │ │ │ │ ├── login.ts │ │ │ │ ├── requestPasswordReset.ts │ │ │ │ ├── resendConfirmationEmail.ts │ │ │ │ └── url.ts │ │ │ ├── index.ts │ │ │ ├── roles │ │ │ │ ├── assignRole.ts │ │ │ │ ├── index.ts │ │ │ │ ├── removeRole.ts │ │ │ │ ├── updateRoles.ts │ │ │ │ └── url.ts │ │ │ └── users │ │ │ │ ├── blockUser.ts │ │ │ │ ├── confirmUser.ts │ │ │ │ ├── createSuperUser.ts │ │ │ │ ├── createUser.ts │ │ │ │ ├── deleteUser.ts │ │ │ │ ├── getAllUsers.ts │ │ │ │ ├── getUser.ts │ │ │ │ ├── index.ts │ │ │ │ ├── resetPassword.ts │ │ │ │ ├── unblockUser.ts │ │ │ │ └── url.ts │ │ ├── baseUrl.ts │ │ ├── helpers │ │ │ ├── apiTypes.ts │ │ │ ├── httpRequest.ts │ │ │ └── sleep.ts │ │ └── index.ts │ └── tsconfig.json └── web │ ├── .env │ ├── .storybook │ ├── main.js │ └── preview.js │ ├── babel.config.js │ ├── components.json │ ├── esbuild.js │ ├── jest.config.ts │ ├── jest.setup.ts │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── bundle.css │ ├── bundle.css.map │ ├── bundle.js │ ├── bundle.js.map │ ├── index.html │ ├── serve.json │ └── tailwind.css │ ├── src │ ├── core │ │ ├── ToDoItem.ts │ │ ├── ToDoItems.ts │ │ ├── __tests__ │ │ │ ├── ToDoItem.test.ts │ │ │ ├── basic.test.ts │ │ │ └── utils.ts │ │ ├── httpProcesses.ts │ │ ├── interfaces.ts │ │ └── utils.ts │ ├── index.css │ ├── index.tsx │ ├── mockApis │ │ └── auth │ │ │ ├── auth │ │ │ ├── login.mock.ts │ │ │ ├── requestPasswordReset.mock.ts │ │ │ └── resendConfirmationEmail.mock.ts │ │ │ ├── localStorage │ │ │ └── localStorage.mock.ts │ │ │ ├── roles │ │ │ ├── assignRole.mock.ts │ │ │ ├── removeRole.mock.ts │ │ │ └── updateRoles.mock.ts │ │ │ └── users │ │ │ ├── blockUser.mock.ts │ │ │ ├── confirmUser.mock.ts │ │ │ ├── createSuperUser.mock.ts │ │ │ ├── createUser.mock.ts │ │ │ ├── deleteUser.mock.ts │ │ │ ├── getAllUsers.mock.ts │ │ │ ├── getUser.mock.ts │ │ │ ├── resetPassword.mock.ts │ │ │ └── unblockUser.mock.ts │ ├── tailwind.css │ └── ui │ │ ├── components │ │ ├── components │ │ │ ├── Button │ │ │ │ └── Button.tsx │ │ │ ├── GlobalModal │ │ │ │ ├── GlobalModal.stories.tsx │ │ │ │ ├── GlobalModal.styles.tsx │ │ │ │ ├── GlobalModal.tsx │ │ │ │ └── OutsideClickHandler.tsx │ │ │ └── LoadingSpinner │ │ │ │ └── LoadingSpinner.tsx │ │ └── shadcnComponents │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── formCardContainer.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── select-modal.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ └── toggle.tsx │ │ ├── helpers │ │ ├── passThroughRef.ts │ │ ├── sleep.ts │ │ └── utils.ts │ │ └── pages │ │ └── AuthPages │ │ ├── ConfirmUserPage │ │ ├── ConfirmUserForm │ │ │ ├── ConfirmUserForm.stories.tsx │ │ │ └── ConfirmUserForm.tsx │ │ ├── ConfirmUserPage.stories.tsx │ │ ├── ConfirmUserPage.styles.tsx │ │ └── ConfirmUserPage.tsx │ │ ├── CreateSuperUserPage │ │ ├── CreateSuperUserForm │ │ │ ├── CreateSuperUserForm.stories.tsx │ │ │ └── CreateSuperUserForm.tsx │ │ ├── CreateSuperUserPage.stories.tsx │ │ ├── CreateSuperUserPage.styles.tsx │ │ └── CreateSuperUserPage.tsx │ │ ├── LoginPage │ │ ├── LoginForm │ │ │ ├── LoginForm.stories.tsx │ │ │ └── LoginForm.tsx │ │ ├── LoginPage.stories.tsx │ │ ├── LoginPage.styles.tsx │ │ └── LoginPage.tsx │ │ ├── SuperAdminPanelPage │ │ ├── Components │ │ │ ├── CreateUserForm │ │ │ │ ├── CreateUserForm.stories.tsx │ │ │ │ ├── CreateUserForm.styles.tsx │ │ │ │ └── CreateUserForm.tsx │ │ │ └── UserTable │ │ │ │ ├── Columns.tsx │ │ │ │ ├── UserTable.stories.tsx │ │ │ │ └── UserTable.tsx │ │ ├── SuperAdminPanelPage.stories.tsx │ │ ├── SuperAdminPanelPage.styles.tsx │ │ └── SuperAdminPanelPage.tsx │ │ ├── UserActionsPage │ │ ├── Components │ │ │ ├── BlockUserForm │ │ │ │ ├── BlockUserForm.stories.tsx │ │ │ │ └── BlockUserForm.tsx │ │ │ ├── UserActionsCard │ │ │ │ ├── UserActionsCard.stories.tsx │ │ │ │ ├── UserActionsCard.styles.tsx │ │ │ │ └── UserActionsCard.tsx │ │ │ ├── UserDetailsCard │ │ │ │ ├── UserDetailsCard.stories.tsx │ │ │ │ ├── UserDetailsCard.styles.tsx │ │ │ │ └── UserDetailsCard.tsx │ │ │ └── UserRolesForm │ │ │ │ ├── UserRolesForm.stories.tsx │ │ │ │ └── UserRolesForm.tsx │ │ ├── UserActionsPage.stories.tsx │ │ ├── UserActionsPage.styles.tsx │ │ └── UserActionsPage.tsx │ │ └── routes.tsx │ ├── storybook.test.js │ ├── tailwind.config.js │ └── tsconfig.json ├── ingress ├── Cargo.toml └── src │ └── main.rs ├── nanoservices ├── auth │ ├── core │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── api │ │ │ ├── auth │ │ │ │ ├── login.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── refresh.rs │ │ │ │ ├── request_password_reset.rs │ │ │ │ └── resend_confirmation_email.rs │ │ │ ├── mod.rs │ │ │ ├── role_permissions │ │ │ │ ├── create_role_permission.rs │ │ │ │ ├── delete_role_permission.rs │ │ │ │ ├── get_role_permissions.rs │ │ │ │ ├── mod.rs │ │ │ │ └── update_roles.rs │ │ │ └── users │ │ │ │ ├── block.rs │ │ │ │ ├── confirm_user.rs │ │ │ │ ├── create.rs │ │ │ │ ├── create_super_admin.rs │ │ │ │ ├── delete_user.rs │ │ │ │ ├── get.rs │ │ │ │ ├── get_all_profiles.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── reset_password.rs │ │ │ │ ├── unblock.rs │ │ │ │ └── update.rs │ │ │ └── lib.rs │ └── networking │ │ ├── Cargo.toml │ │ └── src │ │ ├── api │ │ ├── auth │ │ │ ├── login.rs │ │ │ ├── logout.rs │ │ │ ├── mod.rs │ │ │ ├── refresh.rs │ │ │ ├── request_password_reset.rs │ │ │ └── resend_confirmation_email.rs │ │ ├── mod.rs │ │ ├── roles │ │ │ ├── assign_role.rs │ │ │ ├── mod.rs │ │ │ ├── remove_role.rs │ │ │ └── update_roles.rs │ │ └── users │ │ │ ├── block.rs │ │ │ ├── confirm_user.rs │ │ │ ├── create.rs │ │ │ ├── create_super_admin.rs │ │ │ ├── delete.rs │ │ │ ├── get.rs │ │ │ ├── get_all_profiles.rs │ │ │ ├── mod.rs │ │ │ ├── reset_password.rs │ │ │ ├── unblock.rs │ │ │ └── update.rs │ │ ├── lib.rs │ │ └── utils.rs ├── email │ └── core │ │ ├── Cargo.toml │ │ └── src │ │ ├── api │ │ ├── mailchimp_emails │ │ │ ├── confirmation_email.rs │ │ │ ├── manage_rate_limit.rs │ │ │ ├── mod.rs │ │ │ └── password_reset_email.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── mailchimp_helpers │ │ ├── create_mailchimp_template.rs │ │ ├── mailchimp_template.rs │ │ └── mod.rs │ │ └── mailchimp_traits │ │ ├── mc_definitions.rs │ │ ├── mc_integrations.rs │ │ └── mod.rs └── to_do │ ├── core │ ├── Cargo.toml │ └── src │ │ ├── api │ │ ├── basic_actions │ │ │ ├── complete_to_do_item.rs │ │ │ ├── create.rs │ │ │ ├── delete.rs │ │ │ ├── get_for_user.rs │ │ │ ├── get_pending_items_for_user.rs │ │ │ ├── mod.rs │ │ │ └── reassign.rs │ │ └── mod.rs │ │ └── lib.rs │ └── networking │ ├── Cargo.toml │ └── src │ ├── api │ ├── basic_actions │ │ ├── create.rs │ │ └── mod.rs │ └── mod.rs │ └── lib.rs └── scripts ├── ingress.pid ├── kill_ingress.sh └── run_ingress.sh /.env: -------------------------------------------------------------------------------- 1 | DB_URL=postgres://username:password@localhost:5433/main_db 2 | JWT_SECRET=secret 3 | EMAIL_RATE_LIMIT=5 4 | RATE_LIMIT_PERIOD_MINUTES=60 5 | MAILCHIMP_API_KEY="test_api_key" 6 | SECRET_KEY=secret 7 | PRODUCTION=FALSE -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # RustRover 17 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 18 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 19 | # and can be added to the global gitignore or merged into this file. For a more nuclear 20 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 21 | .idea/ 22 | .DS_Store 23 | logs/ 24 | frontends/apiModules/node_modules/ 25 | frontends/apiModules/dist/ 26 | frontends/web/node_modules/ 27 | frontends/web/storybook-static/ 28 | tests/api/node_modules/ 29 | frontends/modules/server_api/node_modules/ 30 | -------------------------------------------------------------------------------- /.gitignore copy: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # RustRover 17 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 18 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 19 | # and can be added to the global gitignore or merged into this file. For a more nuclear 20 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 21 | .idea/ 22 | .DS_Store 23 | logs/ 24 | frontends/apiModules/node_modules/ 25 | frontends/apiModules/dist/ 26 | frontends/web/node_modules/ 27 | frontends/web/storybook-static/ 28 | tests/api/node_modules/ 29 | frontends/modules/server_api/node_modules/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [workspace] 3 | resolver = "2" 4 | members = [ 5 | "dal/dal", 6 | "ingress", 7 | "dal/kernel", 8 | "nanoservices/auth/core", 9 | "nanoservices/auth/networking", 10 | "nanoservices/email/core", 11 | "nanoservices/to_do/core", 12 | "nanoservices/to_do/networking", 13 | "crates/dal-tx-impl", 14 | "crates/event-subscriber", 15 | "crates/publish-event", 16 | "crates/utils", "crates/compile_api_macros", 17 | ] 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-server-example 2 | 3 | This repo is an example of using macros how to structure a web app so the React frontend is embedded in the Rust binary and served by the Rust server. This repo covers how to use macros to simplify API endpoints and also unit test API endpoints and mock DB calls. 4 | 5 | -------------------------------------------------------------------------------- /crates/compile_api_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "compile_api_macros" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | quote = "1.0.37" 8 | syn = { version = "2.0.95", features = ["full"] } 9 | 10 | [lib] 11 | proc-macro = true 12 | -------------------------------------------------------------------------------- /crates/dal-tx-impl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dal-tx-impl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Maxwell Flitton"] 6 | description = "A collection of utilities for nanoservices" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | quote = "1.0.37" 11 | syn = { version = "2.0.95", features = ["full"] } 12 | 13 | [lib] 14 | proc-macro = true 15 | -------------------------------------------------------------------------------- /crates/dal-tx-impl/README.md: -------------------------------------------------------------------------------- 1 | # Dal Tx Impl 2 | 3 | A basic proc macro crate for implementing async functions into traits. Below, we can define the following trait: 4 | 5 | ```rust 6 | trait TestTrait { 7 | fn test_fn() -> impl Future> + Send; 8 | } 9 | ``` 10 | 11 | We can then implement the `TestTrait` trait for the `TestStruct` using the `impl_transaction` macro with the code below: 12 | 13 | ```rust 14 | #[impl_transaction(TestStruct, TestTrait, test_fn)] 15 | async fn any_function_name() -> Result { 16 | Ok(35) 17 | } 18 | ``` 19 | 20 | This macro is for single async functions only to use against traits with just one function to implement. The name of the function is just for readability as the body is lifted into the trait function implementation so there are no clashes with other functions. This macro can also be helpful with mocking. 21 | -------------------------------------------------------------------------------- /crates/dal-tx-impl/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro::TokenStream; 4 | use quote::quote; 5 | use syn::{ 6 | parse_macro_input, parse::Parse, parse::ParseStream, 7 | ItemFn, Ident, Token, Result 8 | }; 9 | 10 | 11 | struct ImplementTraitArgs { 12 | struct_name: Ident, 13 | trait_name: Ident, 14 | fn_name: Ident, 15 | } 16 | 17 | impl Parse for ImplementTraitArgs { 18 | fn parse(input: ParseStream) -> Result { 19 | let struct_name: Ident = input.parse()?; 20 | input.parse::()?; 21 | let trait_name: Ident = input.parse()?; 22 | input.parse::()?; 23 | let fn_name: Ident = input.parse()?; 24 | Ok(Self { 25 | struct_name, 26 | trait_name, 27 | fn_name, 28 | }) 29 | } 30 | } 31 | 32 | #[proc_macro_attribute] 33 | pub fn impl_transaction(attr: TokenStream, item: TokenStream) -> TokenStream { 34 | // Parse the attribute arguments 35 | let ImplementTraitArgs { 36 | struct_name, 37 | trait_name, 38 | fn_name, 39 | } = parse_macro_input!(attr as ImplementTraitArgs); 40 | 41 | // Parse the input function 42 | let input_fn = parse_macro_input!(item as ItemFn); 43 | 44 | // Extract function components 45 | let fn_inputs = &input_fn.sig.inputs; 46 | let fn_body = &input_fn.block; 47 | 48 | // Extract the function signature generics is there are any 49 | let fn_generics = &input_fn.sig.generics; 50 | 51 | let fn_output = match &input_fn.sig.output { 52 | syn::ReturnType::Type(_, ty) => ty.as_ref(), 53 | syn::ReturnType::Default => { 54 | panic!("Function must have a return type.") 55 | } 56 | }; 57 | 58 | // Generate the expanded code 59 | let expanded = quote! { 60 | impl #trait_name for #struct_name { 61 | fn #fn_name #fn_generics (#fn_inputs) -> impl std::future::Future + Send { 62 | async move #fn_body 63 | } 64 | } 65 | }; 66 | TokenStream::from(expanded) 67 | } 68 | -------------------------------------------------------------------------------- /crates/event-subscriber/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nan-serve-event-subscriber" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Maxwell Flitton"] 6 | description = "Tokio event subscriber for nanoservices" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | quote = "1.0.37" 11 | syn = { version = "2.0.90", features = ["full"] } 12 | 13 | [lib] 14 | proc-macro = true 15 | -------------------------------------------------------------------------------- /crates/event-subscriber/README.md: -------------------------------------------------------------------------------- 1 | # Nanoservices event subscriber 2 | 3 | A basic proc macro crate for subscribing to events. We can subscribe to events with the following code: 4 | 5 | ```rust 6 | #[derive(Serialize, Deserialize, Debug)] 7 | struct Two; 8 | 9 | #[subscribe_to_event] 10 | async fn test2(two: Two) { 11 | println!("calling from test2 function with: {:?}", two); 12 | } 13 | ``` 14 | 15 | This generates the code for a standard function under the name as it is defined as, and a slightly mangled function which is registered with the event subscriber. If an event is then published with the `Two` then all functions subscribed to the `Two` event will be called with the instance of the `Two` struct. 16 | -------------------------------------------------------------------------------- /crates/publish-event/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nan-serve-publish-event" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Maxwell Flitton"] 6 | description = "Tokio event publisher for nanoservices" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | quote = "1.0.37" 11 | syn = { version = "2.0.90", features = ["full"] } 12 | 13 | [lib] 14 | proc-macro = true 15 | -------------------------------------------------------------------------------- /crates/publish-event/README.md: -------------------------------------------------------------------------------- 1 | # Nanoservices event publisher 2 | 3 | A basic proc macro crate for publishing events. We can subscribe to events with the following code: 4 | 5 | ```rust 6 | #[derive(Serialize, Deserialize, Debug)] 7 | struct Two; 8 | 9 | #[subscribe_to_event] 10 | async fn test2(two: Two) { 11 | println!("calling from test2 function with: {:?}", two); 12 | } 13 | ``` 14 | 15 | This generates the code for a standard function under the name as it is defined as, and a slightly mangled function which is registered with the event subscriber. If an event is then published with the `Two` then all functions subscribed to the `Two` event will be called with the instance of the `Two` struct. We can then publish the event with the following code: 16 | 17 | ```rust 18 | let two = Two {}; 19 | publish_event!(two); 20 | ``` 21 | 22 | The `test2` function will then be called with the `Two` struct instance. This is a basic event publisher and subscriber system for nanoservices. 23 | -------------------------------------------------------------------------------- /crates/publish-event/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro::TokenStream; 4 | use quote::quote; 5 | use syn::{parse_macro_input, Ident}; 6 | 7 | 8 | #[proc_macro] 9 | pub fn publish_event(input: TokenStream) -> TokenStream { 10 | let instance_name = parse_macro_input!(input as Ident); 11 | 12 | let expanded = quote! { 13 | { 14 | let type_name = std::any::type_name_of_val(&#instance_name); 15 | let name = type_name.split("::").last().unwrap(); // Extract the last segment (e.g., "AddNumbers") 16 | let data = bincode::serialize(&#instance_name).unwrap(); 17 | crate::tokio_event_adapter_runtime::publish_event(name, data); 18 | } 19 | }; 20 | 21 | TokenStream::from(expanded) 22 | } 23 | -------------------------------------------------------------------------------- /crates/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "utils" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | actix-web = { version = "4.5.1", optional = false } 8 | serde = { version = "1.0.197", features = ["derive"] } 9 | thiserror = "2.0.10" 10 | compile_api_macros = { path = "../compile_api_macros" } 11 | -------------------------------------------------------------------------------- /crates/utils/src/compile_api.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | #[macro_export] 4 | macro_rules! compile_api { 5 | (|$( $arg_name:ident : $arg_type:ty ),*|{ $($body:tt)* }, $func_name:ident, $($trait_tag:tt)+) => { 6 | pub async fn $func_name($( $arg_name : $arg_type ),*) -> Result 7 | where 8 | X: $($trait_tag)+ 9 | { 10 | $($body)* 11 | } 12 | }; 13 | (|| { $($body:tt)* }, $func_name:ident, $($trait_tag:tt)+) => { 14 | pub async fn $func_name() -> Result 15 | where 16 | X: $($trait_tag)+ 17 | { 18 | $($body)* 19 | } 20 | }; 21 | ( 22 | TOKEN($role_check:ty), 23 | |$( $arg_name:ident : $arg_type:ty ),*| { $($body:tt)* }, 24 | $func_name:ident, 25 | $($trait_tag:tt)+) 26 | 27 | => { 28 | pub async fn $func_name( 29 | jwt: kernel::token::token::HeaderToken, $( $arg_name : $arg_type ),* 30 | ) -> Result 31 | where 32 | X: $($trait_tag)+, 33 | Y: utils::config::GetConfigVariable + Send, 34 | Z: kernel::token::session_cache::traits::GetAuthCacheSession 35 | { 36 | let user_session = match Z::get_auth_cache_session(&jwt).await { 37 | Ok(Some(session)) => {session}, 38 | Ok(None) => { 39 | return Err(utils::errors::NanoServiceError::new( 40 | "No longer in session cache".to_string(), 41 | utils::errors::NanoServiceErrorStatus::Unauthorized 42 | )) 43 | }, 44 | Err(e) => { 45 | return Err(e) 46 | } 47 | }; 48 | $($body)* 49 | } 50 | }; 51 | ( 52 | TOKEN($role_check:ty), 53 | || { $($body:tt)* }, 54 | $func_name:ident, 55 | $($trait_tag:tt)+) 56 | 57 | => { 58 | pub async fn $func_name( 59 | jwt: kernel::token::token::HeaderToken) -> Result 60 | where 61 | X: $($trait_tag)+, 62 | Y: utils::config::GetConfigVariable + Send, 63 | Z: kernel::token::session_cache::traits::GetAuthCacheSession 64 | { 65 | match Z::get_auth_cache_session(&jwt).await { 66 | Ok(Some(_)) => {}, 67 | Ok(None) => { 68 | return Err(utils::errors::NanoServiceError::new( 69 | "No longer in session cache".to_string(), 70 | utils::errors::NanoServiceErrorStatus::Unauthorized 71 | )) 72 | }, 73 | Err(e) => { 74 | return Err(e) 75 | } 76 | }; 77 | $($body)* 78 | } 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /crates/utils/src/config.rs: -------------------------------------------------------------------------------- 1 | //! Defines extracting config variables. 2 | use std::env; 3 | use crate::errors::{NanoServiceError, NanoServiceErrorStatus}; 4 | 5 | 6 | /// Defines the trait for getting config variables 7 | pub trait GetConfigVariable { 8 | 9 | /// Gets the config variable 10 | /// 11 | /// # Arguments 12 | /// * `variable` - The name of the config variable to get 13 | /// 14 | /// # Returns 15 | /// * `Result` - The result of getting the config variable 16 | fn get_config_variable(variable: String) -> Result; 17 | } 18 | 19 | 20 | /// Defines the struct for getting config variables from the environment 21 | pub struct EnvConfig; 22 | 23 | 24 | impl GetConfigVariable for EnvConfig { 25 | 26 | /// Gets the config variable from the environment 27 | /// 28 | /// # Arguments 29 | /// * `variable` - The name of the config variable to get 30 | /// 31 | /// # Returns 32 | /// * `Result` - The result of getting the config variable 33 | fn get_config_variable(variable: String) -> Result { 34 | match env::var(&variable) { 35 | Ok(val) => Ok(val), 36 | Err(_) => Err( 37 | NanoServiceError::new( 38 | format!("{} not found in environment", variable), 39 | NanoServiceErrorStatus::Unknown 40 | ) 41 | ) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | pub mod config; 3 | pub mod compile_api; 4 | pub use compile_api_macros::api_endpoint; 5 | pub mod test_api_endpoint; 6 | -------------------------------------------------------------------------------- /dal/dal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dal" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde ={ version="1.0.197", features = ["derive"] } 8 | utils = { path = "../../crates/utils" } 9 | dal-tx-impl = { path = "../../crates/dal-tx-impl" } 10 | kernel = { path = "../kernel" } 11 | 12 | # for sqlx-postgres 13 | sqlx = { version = "0.8.3", features = ["postgres", "json", "runtime-tokio"], optional = false } 14 | once_cell = { version = "1.19.0", optional = false } 15 | 16 | [dev-dependencies] 17 | tokio = { version = "1.43.0", features = ["full"] } 18 | -------------------------------------------------------------------------------- /dal/dal/migrations/20240523088625_initial-setup.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | CREATE TABLE IF NOT EXISTS users ( 3 | id SERIAL PRIMARY KEY, 4 | confirmed BOOLEAN NOT NULL DEFAULT FALSE, 5 | username VARCHAR NOT NULL UNIQUE, 6 | email VARCHAR NOT NULL UNIQUE, 7 | first_name VARCHAR NOT NULL, 8 | last_name VARCHAR NOT NULL, 9 | user_role VARCHAR NOT NULL, 10 | password VARCHAR NOT NULL, 11 | uuid VARCHAR NOT NULL DEFAULT gen_random_uuid() UNIQUE, 12 | date_created TIMESTAMP NOT NULL DEFAULT NOW(), 13 | last_logged_in TIMESTAMP NOT NULL DEFAULT NOW(), 14 | blocked BOOLEAN NOT NULL DEFAULT FALSE 15 | ); 16 | 17 | 18 | CREATE TABLE IF NOT EXISTS role_permissions ( 19 | id SERIAL PRIMARY KEY, 20 | user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, 21 | role VARCHAR NOT NULL, 22 | CONSTRAINT unique_user_role UNIQUE (user_id, role) -- Ensure combination of user_id and role is unique 23 | ); 24 | 25 | 26 | CREATE TABLE IF NOT EXISTS rate_limit_entries ( 27 | id SERIAL PRIMARY KEY, 28 | email VARCHAR NOT NULL, 29 | rate_limit_period_start TIMESTAMP DEFAULT NOW(), 30 | count INTEGER DEFAULT 1 31 | ); 32 | 33 | 34 | CREATE TABLE IF NOT EXISTS todos ( 35 | id SERIAL PRIMARY KEY, 36 | name VARCHAR NOT NULL, 37 | due_date TIMESTAMP, 38 | assigned_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, 39 | assigned_to INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, 40 | description TEXT, 41 | date_assigned TIMESTAMP NOT NULL DEFAULT NOW(), 42 | date_finished TIMESTAMP, 43 | finished BOOLEAN NOT NULL DEFAULT FALSE 44 | ); 45 | -------------------------------------------------------------------------------- /dal/dal/src/connections/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod sqlx_postgres; 2 | -------------------------------------------------------------------------------- /dal/dal/src/connections/sqlx_postgres.rs: -------------------------------------------------------------------------------- 1 | //! Defines the connection to the PostgreSQL database and the `SqlxPostGresDescriptor` for dependency injection. 2 | //! 3 | //! # Overview 4 | //! - Establishes a connection pool for a PostgreSQL database using the `sqlx` library. 5 | //! - Provides the `SqlxPostGresDescriptor` struct to serve as a handle for database-related operations. 6 | //! - Configures the connection pool using environment variables for flexibility and scalability. 7 | //! 8 | //! # Features 9 | //! - The `SQLX_POSTGRES_POOL` is a lazily-initialized static instance for managing database connections. 10 | //! - The `SqlxPostGresDescriptor` is used for dependency injection and applying database traits for transaction handling. 11 | use sqlx::postgres::{PgPool, PgPoolOptions}; 12 | use once_cell::sync::Lazy; 13 | use std::env; 14 | 15 | /// A descriptor struct used for applying database traits and dependency injection. 16 | /// 17 | /// # Notes 18 | /// This struct is intended to be used as a handle for implementing database-related traits 19 | /// that define transactions or other interactions with the database. 20 | pub struct SqlxPostGresDescriptor; 21 | 22 | /// A lazily-initialized static instance of the PostgreSQL connection pool. 23 | /// 24 | /// # Details 25 | /// - Uses the `DB_URL` environment variable to determine the connection string. 26 | /// - Allows configuring the maximum number of connections via the `TO_DO_MAX_CONNECTIONS` environment variable. 27 | /// - Falls back to a default of 5 maximum connections if the environment variable is not set. 28 | /// 29 | /// # Panics 30 | /// - If the `DB_URL` environment variable is not set or the connection pool cannot be created. 31 | pub static SQLX_POSTGRES_POOL: Lazy = Lazy::new(|| { 32 | // Retrieve the database connection string from the environment. 33 | let connection_string = env::var("DB_URL").unwrap(); 34 | 35 | // Determine the maximum number of connections from the environment. 36 | let max_connections = match std::env::var("TO_DO_MAX_CONNECTIONS") { 37 | Ok(val) => val, 38 | Err(_) => "5".to_string(), // Default to 5 if not set. 39 | } 40 | .trim() 41 | .parse::() 42 | .map_err(|_e| "Could not parse max connections".to_string()) 43 | .unwrap(); 44 | 45 | // Configure the connection pool. 46 | let pool = PgPoolOptions::new() 47 | .max_connections(max_connections); 48 | 49 | // Establish the connection pool lazily. 50 | pool.connect_lazy(&connection_string) 51 | .expect("Failed to create pool") 52 | }); 53 | -------------------------------------------------------------------------------- /dal/dal/src/define_transactions.rs: -------------------------------------------------------------------------------- 1 | //! Defines the macro around mapping functions to traits for transactions. 2 | 3 | #[macro_export] 4 | macro_rules! define_dal_transactions { 5 | ( 6 | $( $trait:ident => $func_name:ident $(< $($generic:tt),* >)? ($($param:ident : $ptype:ty),*) -> $rtype:ty ),* $(,)? 7 | ) => { 8 | $( 9 | pub trait $trait { 10 | fn $func_name $(< $($generic),* >)? ($($param : $ptype),*) -> impl std::future::Future> + Send; 11 | } 12 | )* 13 | }; 14 | } 15 | 16 | 17 | #[cfg(test)] 18 | mod tests { 19 | 20 | use dal_tx_impl::impl_transaction; 21 | use utils::errors::NanoServiceError; 22 | use std::future::Future; 23 | 24 | struct TestStruct; 25 | 26 | trait TestTrait { 27 | fn test_fn() -> impl Future> + Send; 28 | } 29 | 30 | #[impl_transaction(TestStruct, TestTrait, test_fn)] 31 | async fn test_fn() -> Result { 32 | Ok(35) 33 | } 34 | 35 | #[tokio::test] 36 | async fn test_impl_transaction() { 37 | let outcome = TestStruct::test_fn().await; 38 | assert_eq!(outcome.unwrap(), 35); 39 | } 40 | 41 | #[tokio::test] 42 | async fn test_define_dal_transactions() { 43 | 44 | struct NewUser; 45 | 46 | define_dal_transactions!( 47 | CreateUser => create(user: NewUser) -> i32, 48 | DeleteUser => delete(id: i32) -> bool 49 | ); 50 | 51 | struct PostgresHandle; 52 | 53 | #[impl_transaction(PostgresHandle, DeleteUser, delete)] 54 | async fn create_user_postgres(_uid: i32) -> Result { 55 | Ok(true) 56 | } 57 | 58 | #[impl_transaction(PostgresHandle, CreateUser, create)] 59 | async fn create_user_postgres(_user: NewUser) -> Result { 60 | Ok(1) 61 | } 62 | let new_user = NewUser; 63 | let outcome = PostgresHandle::create(new_user).await.unwrap(); 64 | assert_eq!(outcome, 1); 65 | 66 | let outcome = PostgresHandle::delete(1).await.unwrap(); 67 | assert_eq!(outcome, true); 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /dal/dal/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod migrations; 2 | pub mod connections; 3 | pub mod users; 4 | pub mod rate_limit_entries; 5 | pub mod role_permissions; 6 | pub mod define_transactions; 7 | pub mod to_do_items; 8 | -------------------------------------------------------------------------------- /dal/dal/src/migrations.rs: -------------------------------------------------------------------------------- 1 | //! Defines the migration functions for databases. 2 | use crate::connections::sqlx_postgres::SQLX_POSTGRES_POOL; 3 | 4 | 5 | /// Runs a set of embedded migrations for a specific database. 6 | pub async fn run_migrations() { 7 | println!("Migrating database..."); 8 | let mut migrations = sqlx::migrate!("./migrations"); 9 | migrations.ignore_missing = true; 10 | let result = migrations.run(&*SQLX_POSTGRES_POOL).await.unwrap(); 11 | println!("to-do database migrations completed: {:?}", result); 12 | } 13 | -------------------------------------------------------------------------------- /dal/dal/src/rate_limit_entries/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod postgres_txs; 2 | pub mod tx_definitions; 3 | -------------------------------------------------------------------------------- /dal/dal/src/rate_limit_entries/tx_definitions.rs: -------------------------------------------------------------------------------- 1 | //! Defines transaction traits for interacting with the `RateLimitEntry` database table. 2 | //! 3 | //! # Overview 4 | //! This file uses the `define_dal_transactions` macro to create traits for database transactions 5 | //! specific to the `RateLimitEntry` entities. Each trait represents a distinct database operation such as 6 | //! creating, updating, and getting email rate limit entries. 7 | //! 8 | //! ## Purpose 9 | //! - Provide an interface for core logic to interact with the data access layer (DAL). 10 | //! - Support dependency injection for database transaction implementations. 11 | //! 12 | //! ## Notes 13 | //! - These traits are designed to be implemented by database descriptor structs, such as `SqlxPostGresDescriptor`. 14 | use kernel::rate_limit_entries::{RateLimitEntry, NewRateLimitEntry}; 15 | use crate::define_dal_transactions; 16 | 17 | 18 | define_dal_transactions!( 19 | CreateRateLimitEntry => create_rate_limit_entry(new_entry: NewRateLimitEntry) -> RateLimitEntry, 20 | GetRateLimitEntry => get_rate_limit_entry(email: String) -> Option, 21 | UpdateRateLimitEntry => update_rate_limit_entry(updated_entry: RateLimitEntry) -> bool, 22 | ); 23 | -------------------------------------------------------------------------------- /dal/dal/src/role_permissions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod postgres_tsx; 2 | pub mod tx_definitions; -------------------------------------------------------------------------------- /dal/dal/src/role_permissions/tx_definitions.rs: -------------------------------------------------------------------------------- 1 | //! Defines transaction traits for interacting with the `RolePermission` database table. 2 | //! 3 | //! # Overview 4 | //! This file uses the `define_dal_transactions` macro to create traits for database transactions 5 | //! specific to the `RolePermission` entities. Each trait represents a distinct database operation such as 6 | //! creating, updating, getting and deleteing role permission entries. 7 | //! 8 | //! ## Purpose 9 | //! - Provide an interface for core logic to interact with the data access layer (DAL). 10 | //! - Support dependency injection for database transaction implementations. 11 | //! 12 | //! ## Notes 13 | //! - These traits are designed to be implemented by database descriptor structs, such as `SqlxPostGresDescriptor`. 14 | use kernel::role_permissions::{RolePermission, NewRolePermission}; 15 | use kernel::users::UserRole; 16 | use crate::define_dal_transactions; 17 | 18 | 19 | define_dal_transactions!( 20 | CreateRolePermission => create_role_permission(role_permission: NewRolePermission) -> RolePermission, 21 | GetRolePermissions => get_role_permissions(user_id: i32) -> Vec, 22 | DeleteRolePermission => delete_role_permission(user_id: i32, role: UserRole) -> bool, 23 | UpdateRolePermissions => update_role_permissions(user_id: i32, roles: Vec) -> () 24 | ); 25 | -------------------------------------------------------------------------------- /dal/dal/src/to_do_items/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tx_definitions; 2 | pub mod postgres_tsx; 3 | -------------------------------------------------------------------------------- /dal/dal/src/to_do_items/tx_definitions.rs: -------------------------------------------------------------------------------- 1 | //! Defines transaction traits for interacting with the `Todo` database table. 2 | //! 3 | //! # Overview 4 | //! This file uses the `define_dal_transactions` macro to create traits for database transactions 5 | //! specific to the `Todo` entities. Each trait represents a distinct database operation such as 6 | //! creating, updating, retrieving, and deleting to-do items. 7 | //! 8 | //! ## Purpose 9 | //! - Provide an interface for core logic to interact with the data access layer (DAL). 10 | //! - Support dependency injection for database transaction implementations. 11 | //! 12 | //! ## Notes 13 | //! - These traits are designed to be implemented by database descriptor structs, such as `SqlxPostGresDescriptor`. 14 | //! - Adding a new database backend requires implementing these traits for the corresponding descriptor. 15 | use kernel::to_do_items::{NewTodo, Todo}; 16 | use crate::define_dal_transactions; 17 | 18 | 19 | define_dal_transactions!( 20 | CreateToDoItem => create_to_do_item(todo: NewTodo) -> Todo, 21 | DeleteToDoItem => delete_to_do_item(id: i32) -> bool, 22 | GetToDoItemsForUser => get_to_do_items_for_user(user_id: i32) -> Vec, 23 | GetPendingToDoItemsForUser => get_pending_to_do_items_for_user(user_id: i32) -> Vec, 24 | ReAssignToDoItem => re_assign_to_do_item(todo_id: i32, new_assigned_to: i32) -> Todo, 25 | CompleteToDoItem => complete_to_do_item(todo_id: i32) -> Todo 26 | ); 27 | -------------------------------------------------------------------------------- /dal/dal/src/users/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tx_definitions; 2 | pub mod postgres_txs; 3 | -------------------------------------------------------------------------------- /dal/dal/src/users/tx_definitions.rs: -------------------------------------------------------------------------------- 1 | //! Defines transaction traits for interacting with the database. 2 | //! 3 | //! # Overview 4 | //! This file uses a macro to define traits for database transactions. These traits specify the 5 | //! actions that can be performed on the `User` entities, such as creating, retrieving, deleting, 6 | //! and confirming users. Each transaction corresponds to a specific database operation and is 7 | //! designed to be implemented by the data access layer (DAL). 8 | //! 9 | //! # Purpose 10 | //! - Streamlines the creation of traits for database transactions. 11 | //! - Provides a consistent interface for interacting with `User` entities in the database. 12 | //! - Supports dependency injection and ensures flexibility when passing these traits to core 13 | //! functions or services. 14 | use crate::define_dal_transactions; 15 | use kernel::users::{NewUser, User, UserProfile}; 16 | 17 | 18 | define_dal_transactions!( 19 | CreateUser => create_user(user: NewUser) -> User, 20 | GetUser => get_user(id: i32) -> User, 21 | GetUserByEmail => get_user_by_email(email: String) -> User, 22 | GetUserByUuid => get_user_by_uuid(uuid: String) -> User, 23 | DeleteUser => delete_user(id: i32) -> bool, 24 | ConfirmUser => confirm_user(uuid: String) -> bool, 25 | GetUserProfileByEmail => get_user_profile_by_email(email: String) -> UserProfile, 26 | GetAllUserProfiles => get_all_user_profiles() -> Vec, 27 | BlockUser => block_user(id: i32) -> bool, 28 | UnblockUser => unblock_user(id: i32) -> bool, 29 | ResetPassword => reset_password(uuid: String, new_password: String) -> bool, 30 | UpdateUuid => update_uuid(email: String, new_uuid: String) -> bool, 31 | UpdateUserUsername => update_user_username(id: i32, username: String) -> bool, 32 | UpdateUserEmail => update_user_email(id: i32, email: String) -> bool, 33 | UpdateUserFirstName => update_user_first_name(id: i32, first_name: String) -> bool, 34 | UpdateUserLasttName => update_user_last_name(id: i32, last_name: String) -> bool, 35 | ); 36 | -------------------------------------------------------------------------------- /dal/kernel/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kernel" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | utils = { path = "../../crates/utils" } 8 | serde ={ version="1.0.197", features = ["derive"] } 9 | argon2 = { version = "0.5.3", features = ["password-hash"]} 10 | uuid = {version = "1.8.0", features = ["serde", "v4"]} 11 | rand = "0.8.5" 12 | sqlx = { version = "0.8.3", features = ["runtime-tokio", "macros", "postgres", "json", "chrono"]} 13 | chrono = { version = "0.4.39", features = ["serde"] } 14 | actix-web = { version = "4.5.1", optional = false } 15 | jsonwebtoken = "9.3.0" 16 | futures = "0.3.31" 17 | uaparser = "0.6.4" 18 | tokio = { version = "1.43.0" } 19 | 20 | [dev-dependencies] 21 | serde_json = "1.0.135" 22 | -------------------------------------------------------------------------------- /dal/kernel/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod users; 2 | pub mod email_invites; 3 | pub mod rate_limit_entries; 4 | pub mod role_permissions; 5 | pub mod token; 6 | pub mod to_do_items; 7 | pub use chrono; -------------------------------------------------------------------------------- /dal/kernel/src/role_permissions.rs: -------------------------------------------------------------------------------- 1 | //! Defines the RolePermission struct for managing user roles in the system. 2 | //! 3 | //! This file provides data structures and utility methods for managing user role assignments and interactions 4 | //! between the kernel workspace and the data access layer. 5 | //! 6 | //! ## Purpose 7 | //! - To associate users with specific roles in the system, ensuring proper authorization and access control. 8 | 9 | use serde::{Serialize, Deserialize}; 10 | use crate::users::UserRole; 11 | 12 | /// Represents the schema for a new role permission entry in the system. 13 | /// 14 | /// # Fields 15 | /// * user_id - The ID of the user. 16 | /// * role - The role assigned to the user. 17 | #[derive(Serialize, Deserialize, Debug, Clone)] 18 | pub struct NewRolePermission { 19 | pub user_id: i32, 20 | pub role: UserRole, 21 | } 22 | 23 | impl NewRolePermission { 24 | /// Creates a new NewRolePermission instance. 25 | /// 26 | /// # Arguments 27 | /// * user_id - The ID of the user. 28 | /// * role - The role assigned to the user. 29 | /// 30 | /// # Returns 31 | /// * NewRolePermission - If valid data is provided. 32 | pub fn new(user_id: i32, role: UserRole) -> NewRolePermission { 33 | NewRolePermission { user_id, role } 34 | } 35 | } 36 | 37 | 38 | /// Represents the schema for a role permission entry in the system. 39 | /// 40 | /// # Fields 41 | /// * id - The unique identifier for the role permission entry. 42 | /// * user_id - The ID of the user. 43 | /// * role - The role assigned to the user. 44 | #[derive(Serialize, Deserialize, Debug, Clone, sqlx::FromRow, PartialEq)] 45 | pub struct RolePermission { 46 | pub id: i32, 47 | pub user_id: i32, 48 | pub role: UserRole, 49 | } 50 | 51 | impl RolePermission { 52 | /// Checks if the user has a specific role. 53 | /// 54 | /// # Arguments 55 | /// * required_role - The role to check against. 56 | /// 57 | /// # Returns 58 | /// - `true` if the user's role matches the required role. 59 | /// - `false` otherwise. 60 | pub fn has_role(&self, required_role: UserRole) -> bool { 61 | self.role == required_role 62 | } 63 | } 64 | 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | #[test] 71 | fn test_new_role_permission_entry() { 72 | let user_id = 42; 73 | let role = UserRole::Admin; 74 | 75 | let new_entry = NewRolePermission::new(user_id, role.clone()); 76 | 77 | assert_eq!(new_entry.user_id, user_id); 78 | assert_eq!(new_entry.role, role); 79 | } 80 | 81 | #[test] 82 | fn test_role_permission_entry_has_role() { 83 | let entry = RolePermission { 84 | id: 1, 85 | user_id: 42, 86 | role: UserRole::Admin, 87 | }; 88 | 89 | assert!(entry.has_role(UserRole::Admin)); 90 | assert!(!entry.has_role(UserRole::Worker)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /dal/kernel/src/token/checks.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the checks that the token service uses to validate user roles. 2 | //! 3 | //! # Notes 4 | //! The `$match_expr:pat` is used as opposed to `$match_expr:expr` to allow for the use of the `|` operator. 5 | //! The `$(,)?` is used to allow for the optional trailing comma in the macro. 6 | use crate::users::UserRole; 7 | use utils::errors::{NanoServiceError, NanoServiceErrorStatus}; 8 | 9 | 10 | macro_rules! construct_checks { 11 | ($( $struct:ident => $match_expr:pat),* $(,)?) => { 12 | $( 13 | pub struct $struct; 14 | 15 | impl CheckUserRole for $struct { 16 | fn check_user_role(role: &UserRole) -> Result<(), NanoServiceError> { 17 | match role { 18 | $match_expr => Ok(()), 19 | _ => Err(NanoServiceError { 20 | status: NanoServiceErrorStatus::Unauthorized, 21 | message: "Role does not have sufficient permissions".to_string() 22 | }) 23 | } 24 | } 25 | } 26 | )* 27 | }; 28 | } 29 | 30 | 31 | pub trait CheckUserRole { 32 | fn check_user_role(role: &UserRole) -> Result<(), NanoServiceError>; 33 | } 34 | 35 | construct_checks!( 36 | SuperAdminRoleCheck => UserRole::SuperAdmin, 37 | AdminRoleCheck => UserRole::SuperAdmin | UserRole::Admin, 38 | WorkerRoleCheck => UserRole::SuperAdmin | UserRole::Admin | UserRole::Worker, 39 | NoRoleCheck => UserRole::SuperAdmin | UserRole::Admin | UserRole::Worker, 40 | ExactSuperAdminRoleCheck => UserRole::SuperAdmin, 41 | ExactAdminRoleCheck => UserRole::Admin, 42 | ExactWorkerRoleCheck => UserRole::Worker 43 | ); 44 | -------------------------------------------------------------------------------- /dal/kernel/src/token/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod token; 2 | pub mod checks; 3 | pub mod session_cache; 4 | -------------------------------------------------------------------------------- /dal/kernel/src/token/session_cache/engine_mem.rs: -------------------------------------------------------------------------------- 1 | use crate::token::session_cache::traits::{GetAuthCacheSession, SetAuthCacheSession}; 2 | use crate::token::session_cache::structs::{AuthCacheSession, IntoAuthCacheKey, IntoAuthCacheSession}; 3 | use utils::errors::NanoServiceError; 4 | use std::future::Future; 5 | use tokio::sync::Mutex; 6 | use std::collections::HashMap; 7 | use std::sync::Arc; 8 | use std::sync::LazyLock; 9 | 10 | use super::traits::DelAuthCacheSession; 11 | 12 | 13 | pub static SESSION_CACHE: LazyLock>>> = LazyLock::new(|| { 14 | Arc::new(Mutex::new(HashMap::new())) 15 | }); 16 | 17 | 18 | pub struct AuthCacheSessionEngineMem; 19 | 20 | 21 | impl GetAuthCacheSession for AuthCacheSessionEngineMem { 22 | fn get_auth_cache_session(key: &X) 23 | -> impl Future, NanoServiceError>> + Send { 24 | let key = key.into_auth_cache_key(); 25 | async move { 26 | let session = SESSION_CACHE.lock().await; 27 | match session.get(&key.key) { 28 | Some(session) => { 29 | let session_ref = session.clone(); 30 | Ok(Some(session_ref)) 31 | }, 32 | None => Ok(None) 33 | } 34 | } 35 | } 36 | } 37 | 38 | 39 | impl SetAuthCacheSession for AuthCacheSessionEngineMem { 40 | fn set_auth_cache_session(key: &X, session: &Y) 41 | -> impl Future> + Send { 42 | let session = session.into_auth_cache_session(); 43 | let key = key.into_auth_cache_key(); 44 | async move { 45 | let mut session_cache = SESSION_CACHE.lock().await; 46 | session_cache.insert(key.key, session); 47 | Ok(()) 48 | } 49 | } 50 | } 51 | 52 | 53 | impl DelAuthCacheSession for AuthCacheSessionEngineMem { 54 | 55 | fn del_auth_cache_session(key: X) 56 | -> impl Future> + Send { 57 | let key = key.into_auth_cache_key(); 58 | async move { 59 | let mut session_cache = SESSION_CACHE.lock().await; 60 | session_cache.remove(&key.key); 61 | Ok(()) 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /dal/kernel/src/token/session_cache/engine_mock.rs: -------------------------------------------------------------------------------- 1 | use crate::token::session_cache::traits::{GetAuthCacheSession, SetAuthCacheSession}; 2 | use crate::token::session_cache::structs::{AuthCacheSession, IntoAuthCacheKey, IntoAuthCacheSession}; 3 | use utils::errors::NanoServiceError; 4 | use std::future::Future; 5 | use tokio::sync::Mutex; 6 | use std::collections::HashMap; 7 | use std::sync::Arc; 8 | use std::sync::LazyLock; 9 | use crate::users::UserRole; 10 | use chrono::Utc; 11 | 12 | 13 | pub static SESSION_CACHE: LazyLock>>> = LazyLock::new(|| { 14 | Arc::new(Mutex::new(HashMap::new())) 15 | }); 16 | 17 | 18 | pub struct PassAuthSessionCheckMock; 19 | 20 | 21 | impl GetAuthCacheSession for PassAuthSessionCheckMock { 22 | fn get_auth_cache_session(key: &X) 23 | -> impl Future, NanoServiceError>> + Send { 24 | let _key = key.into_auth_cache_key(); 25 | async move { 26 | Ok(Some(AuthCacheSession{ 27 | user_id: 1, 28 | role: UserRole::Admin, 29 | time_started: Utc::now(), 30 | time_expire: Utc::now(), 31 | user_agent: "test".to_string() 32 | })) 33 | } 34 | } 35 | } 36 | 37 | 38 | impl SetAuthCacheSession for PassAuthSessionCheckMock { 39 | fn set_auth_cache_session(_key: &X, _session: &Y) 40 | -> impl Future> + Send { 41 | async move { 42 | Ok(()) 43 | } 44 | } 45 | } 46 | 47 | 48 | pub struct FailAuthSessionCheckMock; 49 | 50 | 51 | impl GetAuthCacheSession for FailAuthSessionCheckMock { 52 | fn get_auth_cache_session(key: &X) 53 | -> impl Future, NanoServiceError>> + Send { 54 | let _key = key.into_auth_cache_key(); 55 | async move { 56 | Ok(Some(AuthCacheSession{ 57 | user_id: 1, 58 | role: UserRole::Admin, 59 | time_started: Utc::now(), 60 | time_expire: Utc::now(), 61 | user_agent: "test".to_string() 62 | })) 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /dal/kernel/src/token/session_cache/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod engine_mem; 2 | pub mod traits; 3 | pub mod structs; 4 | pub mod engine_mock; 5 | -------------------------------------------------------------------------------- /dal/kernel/src/token/session_cache/structs.rs: -------------------------------------------------------------------------------- 1 | use crate::users::UserRole; 2 | use chrono::{DateTime, Utc}; 3 | 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct AuthCacheSession { 7 | pub user_id: i32, 8 | pub role: UserRole, 9 | pub time_started: DateTime, 10 | pub time_expire: DateTime, 11 | pub user_agent: String, 12 | } 13 | 14 | 15 | pub struct AuthCacheKey { 16 | pub key: String 17 | } 18 | 19 | 20 | pub trait IntoAuthCacheSession { 21 | fn into_auth_cache_session(&self) -> AuthCacheSession; 22 | } 23 | 24 | pub trait IntoAuthCacheKey { 25 | fn into_auth_cache_key(&self) -> AuthCacheKey; 26 | } 27 | 28 | impl IntoAuthCacheKey for String { 29 | fn into_auth_cache_key(&self) -> AuthCacheKey { 30 | AuthCacheKey { 31 | key: self.clone() 32 | } 33 | } 34 | } 35 | 36 | impl IntoAuthCacheKey for &str { 37 | fn into_auth_cache_key(&self) -> AuthCacheKey { 38 | AuthCacheKey { 39 | key: self.to_string() 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dal/kernel/src/token/session_cache/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::token::session_cache::structs::{AuthCacheSession, IntoAuthCacheKey, IntoAuthCacheSession}; 2 | use utils::errors::NanoServiceError; 3 | use std::future::Future; 4 | 5 | 6 | pub trait GetAuthCacheSession { 7 | fn get_auth_cache_session(key: &X) 8 | -> impl Future, NanoServiceError>> + Send; 9 | } 10 | 11 | pub trait SetAuthCacheSession { 12 | fn set_auth_cache_session(key: &X, session: &Y) 13 | -> impl Future> + Send; 14 | } 15 | 16 | pub trait DelAuthCacheSession { 17 | fn del_auth_cache_session(key: X) 18 | -> impl Future> + Send; 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | 5 | postgres: 6 | container_name: 'raf-crm-postgres' 7 | image: 'postgres:16.3' 8 | restart: always 9 | ports: 10 | - '5433:5432' 11 | environment: 12 | - 'POSTGRES_USER=username' 13 | - 'POSTGRES_DB=main_db' 14 | - 'POSTGRES_PASSWORD=password' 15 | -------------------------------------------------------------------------------- /frontends/apiModules/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./localStorage"; -------------------------------------------------------------------------------- /frontends/apiModules/auth/localStorage.ts: -------------------------------------------------------------------------------- 1 | export function setJwt(jwt: string): void { 2 | localStorage.setItem('jwt', jwt) 3 | } 4 | 5 | export function getJwt(): string | null { 6 | return localStorage.getItem('jwt') 7 | } 8 | 9 | type Role = "Super Admin" | "Admin" | "Worker"; 10 | 11 | export function setRole(role: Role): void { 12 | localStorage.setItem('role', role) 13 | } 14 | 15 | export function getRole(): string | null { 16 | return localStorage.getItem('role') 17 | } -------------------------------------------------------------------------------- /frontends/apiModules/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./serverApi"; 2 | export * from "./auth"; 3 | -------------------------------------------------------------------------------- /frontends/apiModules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-modules", 3 | "version": "1.0.0", 4 | "description": "All HTTP requests to servers", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "echo \"No test specified\" && exit 0" 10 | }, 11 | "dependencies": { 12 | "axios": "1.7.9", 13 | "base64-js": "1.5.1", 14 | "buffer": "^6.0.3", 15 | "zod": "^3.24.1" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^22.13.0", 19 | "typescript": "^5.7.3" 20 | }, 21 | "keywords": [ 22 | "auth", 23 | "users", 24 | "typescript" 25 | ], 26 | "author": "", 27 | "license": "MIT" 28 | } 29 | -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/auth/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./login"; 2 | export * from "./requestPasswordReset"; 3 | export * from "./resendConfirmationEmail"; -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/auth/auth/login.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * loginUser.ts 3 | * 4 | * Provides functionality to log in a user via the `/login` endpoint. 5 | * Uses Basic Auth for `email` and `password` in the request header, 6 | * along with an optional `role` in the request body. 7 | */ 8 | 9 | import { AuthUrl } from "./url"; 10 | import { ApiResponse, ApiFunction, ErrorResponse } from "../../helpers/apiTypes"; 11 | import { httpRequest, HttpRequestParams } from "../../helpers/httpRequest"; 12 | import { z } from "zod"; 13 | import { Buffer } from 'buffer'; 14 | 15 | /** 16 | * Zod schema for the input data when logging in. 17 | * - `email`: Validates email using Zod's built-in email validator. 18 | * - `password`: A simple string. 19 | * - `role`: A string representing the user role. 20 | */ 21 | const loginInputSchema = z.object({ 22 | email: z.string().email(), 23 | password: z.string(), 24 | role: z.enum([ 25 | "Super Admin", 26 | "Admin", 27 | "Worker" 28 | ]), 29 | }); 30 | 31 | /** 32 | * Zod schema for the output data after a successful login. 33 | * - `token`: A string representing the returned JWT or session token. 34 | */ 35 | const loginOutputSchema = z.object({ 36 | token: z.string(), 37 | role: z.enum([ 38 | "Super Admin", 39 | "Admin", 40 | "Worker" 41 | ]), 42 | }).strict(); 43 | 44 | /** Types inferred from the Zod schemas */ 45 | export type LoginInputSchema = z.infer; 46 | export type LoginOutputSchema = z.infer; 47 | 48 | /** Type definition for the function that handles user login. */ 49 | export type LoginFunction = ApiFunction; 50 | 51 | /** 52 | * Logs in a user by sending a POST request with Basic Auth. 53 | * 54 | * @param user - An object conforming to `LoginInputSchema`, containing the user's credentials and role. 55 | * @returns A `Promise` resolving to `ApiResponse` if login is successful, or `ErrorResponse` on error. 56 | * 57 | */ 58 | export async function loginUser( 59 | user: LoginInputSchema 60 | ): Promise> { 61 | // Basic Auth credentials 62 | const credentials = `${user.email}:${user.password}`; 63 | const encodedCredentials = Buffer.from(credentials).toString("base64"); 64 | const authHeaderValue = `Basic ${encodedCredentials}`; 65 | 66 | // Prepare request params 67 | const params: HttpRequestParams = { 68 | url: new AuthUrl().loginUser, 69 | httpMethod: "post", 70 | args: { role: user.role }, // The request body 71 | customHeaders: { 72 | Authorization: authHeaderValue, 73 | }, 74 | }; 75 | 76 | // Execute the request 77 | const response = await httpRequest(params); 78 | 79 | // Expect a 200 status for successful login 80 | if (response.status === 200) { 81 | const parsedOutput = loginOutputSchema.safeParse(response.body); 82 | if (!parsedOutput.success) { 83 | return { 84 | status: 500, 85 | body: `Return body validation error - ${parsedOutput.error.message}`, 86 | } as ErrorResponse; 87 | } 88 | return { status: response.status, body: parsedOutput.data }; 89 | } 90 | 91 | // Set the jwt and return the error or unexpected status as is 92 | return response as ErrorResponse; 93 | } -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/auth/auth/url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AuthUrl.ts 3 | * 4 | * # Overview 5 | * - Provides URL endpoints for user-related API requests. 6 | * - Extends the VOneUrl class to include base URL configuration for version 1 of the authentication API. 7 | */ 8 | 9 | import { VOneUrl } from "../../baseUrl"; 10 | 11 | /** 12 | * Configures the URL endpoints for the User API. 13 | * 14 | * # Overview 15 | * - Constructs the base URL for user-related API endpoints by appending `/v1/auth/auth` to the root URL. 16 | * - Provides endpoints for creating a standard user and a super user. 17 | * 18 | * # Fields 19 | * - `base`: The base URL for the User API. 20 | * - `loginUser`: The URL endpoint for logging in a user. 21 | */ 22 | export class AuthUrl extends VOneUrl { 23 | 24 | public base: string; 25 | public loginUser: string; 26 | public requestPasswordReset: string; 27 | public resendConfirmationEmail: string; 28 | 29 | /** 30 | * Instantiates the UserUrl class and configures the user-auth-related endpoints. 31 | * 32 | * # Behavior 33 | * - Calls the parent `VOneUrl` constructor. 34 | * - Appends `/v1/auth/auth` to the base URL obtained from `defineRoot()`. 35 | * - Constructs the endpoint for creating a new user by appending "login" to the base URL. 36 | */ 37 | constructor() { 38 | super(); 39 | this.base = this.defineRoot() + "/api/auth/v1/auth"; 40 | this.loginUser = this.base + "/login"; 41 | this.requestPasswordReset = this.base + "/request_password_reset"; 42 | this.resendConfirmationEmail = this.base + "/resend_confirmation_email"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./users"; 2 | export * from "./auth"; 3 | export * from "./roles"; 4 | -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/auth/roles/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./assignRole"; 2 | export * from "./removeRole"; 3 | export * from "./updateRoles"; -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/auth/roles/url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AuthUrl.ts 3 | * 4 | * # Overview 5 | * - Provides URL endpoints for role-related API requests. 6 | * - Extends the VOneUrl class to include base URL configuration for version 1 of the authentication API. 7 | */ 8 | 9 | import { VOneUrl } from "../../baseUrl"; 10 | 11 | /** 12 | * Configures the URL endpoints for the Roles API. 13 | * 14 | * # Overview 15 | * - Constructs the base URL for user-related API endpoints by appending `/v1/auth/roles` to the root URL. 16 | * 17 | * # Fields 18 | * - `base`: The base URL for the User API. 19 | * - `loginUser`: The URL endpoint for logging in a user. 20 | */ 21 | export class RoleUrl extends VOneUrl { 22 | 23 | public base: string; 24 | public assignRole: string; 25 | public removeRole: string; 26 | public updateUserRoles: string; 27 | 28 | /** 29 | * Instantiates the RoleUrl class and configures the role-auth-related endpoints. 30 | * 31 | * # Behavior 32 | * - Calls the parent `VOneUrl` constructor. 33 | * - Appends `/v1/auth/auth` to the base URL obtained from `defineRoot()`. 34 | * - Constructs the endpoint for creating a new role by appending "login" to the base URL. 35 | */ 36 | constructor() { 37 | super(); 38 | this.base = this.defineRoot() + "/api/auth/v1/roles"; 39 | this.assignRole = this.base + "/assign_role"; 40 | this.removeRole = this.base + "/remove_role"; 41 | this.updateUserRoles = this.base + "/update" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/auth/users/blockUser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * blockUser.ts 3 | * 4 | * # Overview 5 | * This module provides functionality to interact with the user blocking API endpoint using Axios. 6 | * It targets the `/block_user` endpoint and ensures that a successful blocking action returns a `200 OK` status. 7 | * Robust error handling is included to manage unexpected responses. 8 | */ 9 | 10 | import { UserUrl } from "./url"; 11 | import { ApiResponse, ApiFunction, ErrorResponse } from "../../helpers/apiTypes"; 12 | import { httpRequest, HttpRequestParams } from "../../helpers/httpRequest"; 13 | import { z } from "zod"; 14 | 15 | /** 16 | * Zod schema for the input data when blocking a user. 17 | * 18 | * - `user_id`: A number representing the unique identifier of the user to block. 19 | */ 20 | const blockUserInputSchema = z.object({ 21 | user_id: z.number(), 22 | }); 23 | 24 | /** 25 | * Zod schema for the output data after a user is blocked. 26 | * (Currently defined as an empty object; adjust as needed.) 27 | */ 28 | const blockUserOutputSchema = z.object({}).strict(); 29 | 30 | // Export types inferred from the Zod schemas. 31 | export type BlockUserInputSchema = z.infer; 32 | export type BlockUserOutputSchema = z.infer; 33 | 34 | /** 35 | * Type definition for the function that blocks a user. 36 | */ 37 | export type BlockUserFunction = ApiFunction; 38 | 39 | /** 40 | * Blocks a user by sending a POST request to the user block endpoint. 41 | * 42 | * # Arguments 43 | * - `user`: An object conforming to the `BlockUserInputSchema` interface containing the user's blocking details. 44 | * 45 | * # Returns 46 | * - A `Promise` resolving to an `ApiResponse` if the user is blocked successfully. 47 | * Otherwise, it resolves to an `ErrorResponse`. 48 | * 49 | * # Errors 50 | * - Returns an error response if the API call does not return a `200 OK` status or if an unexpected error occurs. 51 | * 52 | * # Usage 53 | * ```typescript 54 | * import { blockUser, BlockUserInputSchema } from "./path-to-module"; 55 | * 56 | * const user: BlockUserInputSchema = { 57 | * user_id: 2 58 | * }; 59 | * 60 | * blockUser(user, jwt) 61 | * .then(response => console.log("User blocked successfully:", response)) 62 | * .catch(error => console.error("Error blocking user:", error)); 63 | * ``` 64 | */ 65 | export async function blockUser(user: BlockUserInputSchema, jwt: string): Promise> { 66 | const url = new UserUrl().blockUser; 67 | const params: HttpRequestParams = { 68 | url: url, 69 | httpMethod: "post", 70 | args: user, 71 | jwt: jwt 72 | }; 73 | const response = await httpRequest(params); 74 | 75 | // Validate the output schema with Zod 76 | if (response.status === 200) { 77 | const parsedOutput = blockUserOutputSchema.safeParse(response.body); 78 | if (!parsedOutput.success) { 79 | return { 80 | status: 500, 81 | body: `Return body validation error - ${parsedOutput.error.message}`, 82 | } as ErrorResponse; 83 | } 84 | return { status: response.status, body: parsedOutput.data }; 85 | } 86 | return response as ErrorResponse; 87 | } -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/auth/users/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createSuperUser"; 2 | export * from "./createUser"; 3 | export * from "./getUser"; 4 | export * from "./confirmUser"; 5 | export * from "./getAllUsers"; 6 | export * from "./deleteUser"; 7 | export * from "./unblockUser"; 8 | export * from "./resetPassword"; 9 | export * from "./url"; 10 | 11 | -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/auth/users/url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * UserUrl.ts 3 | * 4 | * # Overview 5 | * - Provides URL endpoints for user-related API requests. 6 | * - Extends the VOneUrl class to include base URL configuration for version 1 of the authentication API. 7 | */ 8 | 9 | import { VOneUrl } from "../../baseUrl"; 10 | 11 | /** 12 | * Configures the URL endpoints for the User API. 13 | * 14 | * # Overview 15 | * - Constructs the base URL for user-related API endpoints by appending `/v1/auth/users` to the root URL. 16 | * - Provides endpoints for creating a standard user and a super user. 17 | * 18 | * # Fields 19 | * - `base`: The base URL for the User API. 20 | * - `createUser`: The URL endpoint for creating a new user. 21 | * - `createSuperUser`: The URL endpoint for creating a super user. 22 | */ 23 | export class UserUrl extends VOneUrl { 24 | 25 | public base: string; 26 | public createUser: string; 27 | public createSuperUser: string; 28 | public confirmUser: string; 29 | public blockUser: string; 30 | public unblockUser: string; 31 | public getUserByUuid: string; 32 | public getUserById: string; 33 | public getUserByEmail: string; 34 | public getUserByJwt: string; 35 | public getAllUsers: string; 36 | public deleteUser: string; 37 | public resetPassword: string; 38 | 39 | /** 40 | * Instantiates the UserUrl class and configures the user-related endpoints. 41 | * 42 | * # Behavior 43 | * - Calls the parent `VOneUrl` constructor. 44 | * - Appends `/v1/auth/users` to the base URL obtained from `defineRoot()`. 45 | * - Constructs the endpoint for creating a new user by appending "create" to the base URL. 46 | * - Constructs the endpoint for creating a super user by appending "super-admin/create" to the base URL. 47 | */ 48 | constructor() { 49 | super(); 50 | this.base = this.defineRoot() + "/api/auth/v1/users"; 51 | this.createUser = this.base + "/create"; 52 | this.createSuperUser = this.base + "/create/superadmin"; 53 | this.confirmUser = this.base + "/confirm"; 54 | this.blockUser = this.base + "/block"; 55 | this.unblockUser = this.base + "/unblock"; 56 | this.getUserById = this.base + "/get-by-id/"; 57 | this.getUserByEmail = this.base + "/get-by-email/"; 58 | this.getUserByJwt = this.base + "/get-by-jwt"; 59 | this.getUserByUuid = this.base + "/get-by-uuid/"; 60 | this.getAllUsers = this.base + "/get-all"; 61 | this.deleteUser = this.base + "/delete"; 62 | this.resetPassword = this.base + "/reset-password"; 63 | } 64 | 65 | constructGetUserById(id: number): string { 66 | return this.getUserById + id; 67 | } 68 | 69 | constructGetUserByEmail(email: string): string { 70 | return this.getUserByEmail + email; 71 | } 72 | 73 | constructGetUserByUuid(uuid: string): string { 74 | return this.getUserByUuid + uuid; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/baseUrl.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configures the URLs for the V0 API. 3 | * 4 | * # Overview 5 | * - Derives the base URL for API requests from the environment variable `BASE_BACKEND_URL`. 6 | * 7 | * # Fields 8 | * - `base`: The root URL for API calls. 9 | * 10 | * # Errors 11 | * - Throws an error if `BASE_BACKEND_URL` is not defined or is empty. 12 | */ 13 | export class VOneUrl { 14 | 15 | public base: string; 16 | 17 | /** 18 | * Instantiates the VOneUrl class and initializes the base URL. 19 | * 20 | * # Behavior 21 | * - Sets the `base` property by invoking the `defineRoot()` method. 22 | * 23 | * # Errors 24 | * - Propagates errors thrown by `defineRoot()` if the environment variable is missing or empty. 25 | */ 26 | constructor() { 27 | this.base = this.defineRoot(); 28 | } 29 | 30 | /** 31 | * Determines the root URL based on the environment configuration. 32 | * 33 | * # Returns 34 | * - The root URL as defined by the `BASE_BACKEND_URL` environment variable. 35 | * 36 | * # Errors 37 | * - Throws an `Error` if `BASE_BACKEND_URL` is not defined or is an empty string. 38 | */ 39 | public defineRoot() { 40 | if (typeof process.env.BASE_BACKEND_URL === "string" && process.env.BASE_BACKEND_URL !== "") { 41 | return process.env.BASE_BACKEND_URL; 42 | } else { 43 | return "http://0.0.0.0:8001"; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/helpers/apiTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A TypeScript file with useful types for HTTP Requests. 3 | */ 4 | 5 | /** 6 | * Defines the possible status codes for HTTP Responses. 7 | */ 8 | type StatusCode = 0 | 200 | 201 | 401 | 404 | 500 9 | type SuccessStatusCode = 200 | 201 10 | type ErrorStatusCode = Exclude 11 | 12 | /** 13 | * Defines the possible HTTP methods. 14 | */ 15 | export type HttpMethod = "get" | "post" 16 | 17 | /** 18 | * Defines the possible HTTP and Api method responses. 19 | */ 20 | export type ErrorResponse = { status: ErrorStatusCode; body: string } 21 | export type HttpResponse = { status: SuccessStatusCode; body: Body } | { status: number; body: any } 22 | export type ApiResponse = { status: SuccessStatusCode; body: Body } | ErrorResponse 23 | 24 | /** 25 | * Defines the possible Api Function types. 26 | */ 27 | export type ApiFunction = (args: Input) => Promise> 28 | export type ApiFunctionNoInput = () => Promise> 29 | -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/helpers/sleep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Delays execution by pausing for a specified duration. 3 | * 4 | * # Arguments 5 | * - `timeMs`: The number of milliseconds to wait before the promise resolves. 6 | * 7 | * # Returns 8 | * - A `Promise` that resolves after the specified delay. 9 | */ 10 | export function sleep(timeMs: number) { 11 | return new Promise((resolve) => setTimeout(resolve, timeMs)) 12 | }a -------------------------------------------------------------------------------- /frontends/apiModules/serverApi/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | -------------------------------------------------------------------------------- /frontends/apiModules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "CommonJS", 5 | "strict": true, 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "declaration": true, 10 | "lib": ["ES2015"], 11 | "baseUrl": ".", 12 | "paths": { 13 | "auth/*": ["./auth/*"], 14 | "serverApi/*": ["./serverApi/*"], 15 | } 16 | }, 17 | "include": ["**/*", "index.ts"], 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /frontends/web/.env: -------------------------------------------------------------------------------- 1 | # .env 2 | # Core Settings 3 | PRODUCTION="FALSE" 4 | 5 | # Front-end URLs 6 | BASE_FRONTEND_URL_DEV="http://localhost:3000" 7 | BASE_FRONTEND_URL_PROD="https://app/fe.com" 8 | 9 | # Back-end URLs 10 | BASE_BACKEND_URL_DEV="http://0.0.0.0:8001" 11 | BASE_BACKEND_URL_PROD="https://app/be.com" 12 | -------------------------------------------------------------------------------- /frontends/web/.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); 3 | 4 | module.exports = { 5 | stories: ["../src/**/*.stories.@(js|jsx|ts|tsx|mdx)"], 6 | addons: [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/addon-interactions", 10 | { 11 | name: "@storybook/addon-postcss", 12 | options: { 13 | postcssLoaderOptions: { 14 | postcssOptions: { 15 | config: path.resolve(__dirname, "../postcss.config.js"), 16 | }, 17 | }, 18 | }, 19 | }, 20 | ], 21 | framework: { 22 | name: "@storybook/react-webpack5", 23 | options: {} 24 | }, 25 | webpackFinal: async (config) => { 26 | config.module.rules.push( 27 | { 28 | test: /\.(ts|tsx)$/, 29 | exclude: /node_modules/, 30 | use: [ 31 | { 32 | loader: "babel-loader", 33 | options: { 34 | presets: [ 35 | "@babel/preset-env", 36 | [ 37 | "@babel/preset-react", 38 | { 39 | runtime: "automatic", 40 | importSource: "@emotion/react" 41 | } 42 | ], 43 | "@babel/preset-typescript" 44 | ], 45 | }, 46 | }, 47 | ], 48 | }, 49 | { 50 | test: /\.(js|jsx)$/, 51 | exclude: /node_modules/, 52 | use: { 53 | loader: "babel-loader", 54 | options: { 55 | presets: ["@babel/preset-env", "@babel/preset-react"], 56 | }, 57 | }, 58 | } 59 | ); 60 | 61 | // Make sure your config.resolve object exists 62 | if (!config.resolve) { 63 | config.resolve = {}; 64 | } 65 | 66 | // Initialize config.resolve.plugins if needed 67 | if (!config.resolve.plugins) { 68 | config.resolve.plugins = []; 69 | } 70 | 71 | // Add tsconfig-paths-webpack-plugin 72 | config.resolve.plugins.push( 73 | new TsconfigPathsPlugin({ 74 | configFile: path.resolve(__dirname, "../tsconfig.json"), 75 | }) 76 | ); 77 | 78 | config.resolve.extensions.push(".ts", ".tsx", ".js", ".jsx"); 79 | return config; 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /frontends/web/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import setupLocatorUI from "@locator/runtime"; 2 | import React from "react"; 3 | import { MemoryRouter } from "react-router-dom"; 4 | import "src/index.css"; 5 | import "src/tailwind.css"; 6 | 7 | 8 | if (process.env.NODE_ENV === "development") { 9 | setupLocatorUI(); 10 | } 11 | 12 | /** @type { import('@storybook/react').Preview } */ 13 | const preview = { 14 | parameters: { 15 | actions: { argTypesRegex: "^on[A-Z].*" }, 16 | controls: { 17 | matchers: { 18 | color: /(background|color)$/i, 19 | date: /Date$/i, 20 | }, 21 | }, 22 | }, 23 | }; 24 | 25 | function WaitForModalRoot({ children }) { 26 | const [modalRootAdded, setModalRootAdded] = React.useState(false); 27 | 28 | React.useEffect(() => { 29 | if (document.getElementById("modal-root")) { 30 | setModalRootAdded(true); 31 | } 32 | }, []); 33 | 34 | return ( 35 | <> 36 | {/* Matches the modal-root div in the index.html file */} 37 | 45 | 46 | ); 47 | } 48 | 49 | export const decorators = [ 50 | (Story, context) => { 51 | const useCustomRouter = context.parameters.useCustomRouter; 52 | 53 | const storyContent = ( 54 | 55 | 56 | 57 | ); 58 | 59 | // Wrap with MemoryRouter unless useCustomRouter is true. This allows 60 | // stories to use the router without needing to wrap the story content 61 | // in a MemoryRouter in each story file. However if useCustomRouter is 62 | // true, the story content will not be wrapped in a MemoryRouter, and 63 | // the story file will be responsible for wrapping the content in 64 | // a custom router. This is useful for stories that need to use a 65 | // specific route such as "/dashboard". 66 | if (!useCustomRouter) { 67 | return ( 68 |
69 | 70 | {storyContent} 71 | 72 |
73 | ); 74 | } 75 | 76 | // For stories specifying useCustomRouter, just render the story content without MemoryRouter 77 | return storyContent; 78 | }, 79 | ]; -------------------------------------------------------------------------------- /frontends/web/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ['@babel/preset-react', {runtime: 'automatic'}] 6 | ], 7 | }; -------------------------------------------------------------------------------- /frontends/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "src/ui/components", 15 | "utils": "src/ui/helpers/utils", 16 | "ui": "src/ui/components/shadcnComponents", 17 | "lib": "src/ui/lib", 18 | "hooks": "src/ui/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /frontends/web/esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const cssModulesPlugin = require('esbuild-css-modules-plugin'); 3 | const dotenv = require("dotenv"); 4 | const { argv } = require("process"); 5 | 6 | 7 | // Get cli args 8 | const args = argv.slice(2); 9 | const isWatchMode = args.includes("--watch"); 10 | 11 | // Load env variables from .env 12 | const envResult = dotenv.config(); 13 | if (envResult.error) { 14 | throw new Error(`Error loading .env file: ${envResult.error}`); 15 | } 16 | 17 | // env checks 18 | if (!process.env.BASE_FRONTEND_URL_DEV) { 19 | throw new Error("BASE_FRONTEND_URL_DEV not set"); 20 | } 21 | if (!process.env.BASE_FRONTEND_URL_PROD) { 22 | throw new Error("BASE_FRONTEND_URL_PROD not set"); 23 | } 24 | if (!process.env.BASE_BACKEND_URL_DEV) { 25 | throw new Error("BASE_BACKEND_URL_DEV not set"); 26 | } 27 | if (!process.env.BASE_BACKEND_URL_PROD) { 28 | throw new Error("BASE_BACKEND_URL_PROD not set"); 29 | } 30 | 31 | // Setting Urls 32 | const isProduction = process.env.PRODUCTION?.toLowerCase() === "true"; 33 | 34 | const baseBackendUrl = isProduction 35 | ? process.env.BASE_BACKEND_URL_PROD 36 | : process.env.BASE_BACKEND_URL_DEV; 37 | 38 | const baseFrontendUrl = isProduction 39 | ? (process.env.BASE_FRONTEND_URL_PROD) 40 | : (process.env.BASE_FRONTEND_URL_DEV); 41 | 42 | 43 | async function build() { 44 | const buildOpts = { 45 | plugins: [ 46 | cssModulesPlugin() 47 | ], 48 | entryPoints: ['src/index.tsx'], 49 | bundle: true, 50 | outfile: 'public/bundle.js', 51 | format: 'esm', 52 | define: { 53 | 'process.env.NODE_ENV': JSON.stringify(isProduction ? "production" : "development"), 54 | 'process.env.PRODUCTION': JSON.stringify(process.env.PRODUCTION), 55 | 'process.env.BASE_BACKEND_URL': JSON.stringify(baseBackendUrl), 56 | 'process.env.BASE_FRONTEND_URL': JSON.stringify(baseFrontendUrl), 57 | }, 58 | minify: true, 59 | sourcemap: true, 60 | loader: { 61 | '.js': 'jsx', 62 | '.tsx': 'tsx', 63 | '.ts': 'ts', 64 | '.wasm': 'binary', 65 | '.css': 'css', 66 | ".gif": "file", 67 | ".jpg": "file", 68 | ".png": "file", 69 | }, 70 | } 71 | 72 | if (isWatchMode) { 73 | console.info("Watching for changes... (use with serve)"); 74 | const ctx = await esbuild.context(buildOpts); 75 | return new Promise(async (res) => { 76 | await ctx.watch(); 77 | }); 78 | } else { 79 | await esbuild.build(buildOpts); 80 | } 81 | } 82 | 83 | 84 | build() 85 | .then(() => { 86 | console.log("Build completed successfully"); 87 | process.exit(0); 88 | }) 89 | .catch((error) => { 90 | console.error("Build failed:", error); 91 | process.exit(1); 92 | }); 93 | -------------------------------------------------------------------------------- /frontends/web/jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jest-environment-jsdom', 4 | setupFilesAfterEnv: ['/jest.setup.ts'], 5 | transform: { 6 | '^.+\\.(ts|tsx)$': ['ts-jest', { 7 | tsconfig: 'tsconfig.json' 8 | // any other ts-jest specific options go here 9 | }], 10 | }, 11 | moduleNameMapper: { 12 | // Add if you have styles or assets imported in your components 13 | "\\.(css|less|scss|sass)$": "identity-obj-proxy" 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /frontends/web/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | module.exports = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'jest-environment-jsdom', 6 | setupFilesAfterEnv: ['/jest.setup.ts'], // Updated to .ts 7 | moduleFileExtensions: ['ts', 'js', 'json', 'node'], 8 | transform: { 9 | '^.+\\.ts$': 'ts-jest', 10 | }, 11 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts$', 12 | // ... rest of the configuration 13 | }; 14 | -------------------------------------------------------------------------------- /frontends/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "@tailwindcss/postcss": {}, 5 | autoprefixer: {}, 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /frontends/web/public/bundle.css: -------------------------------------------------------------------------------- 1 | :root{--color-white: #ffffff;--color-light-grey: #f5f5f5;--color-light-grey-rgb: 245, 245, 245;--overlay-light-grey: rgba(var(--color-light-grey-rgb), .6)}@keyframes jiggle{0%{transform:translate(0)}25%{transform:translate(-5px)}50%{transform:translate(0)}75%{transform:translate(5px)}to{transform:translate(0)}}.global-jiggle{animation:jiggle .4s ease-in-out 2} 2 | /*# sourceMappingURL=bundle.css.map */ 3 | -------------------------------------------------------------------------------- /frontends/web/public/bundle.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["../src/index.css"], 4 | "sourcesContent": [":root {\n --color-white: #ffffff;\n --color-light-grey: #f5f5f5;\n --color-light-grey-rgb: 245, 245, 245;\n --overlay-light-grey: rgba(var(--color-light-grey-rgb), 0.6);\n}\n\n@keyframes jiggle {\n 0% { transform: translateX(0); }\n 25% { transform: translateX(-5px); }\n 50% { transform: translateX(0); }\n 75% { transform: translateX(5px); }\n 100% { transform: translateX(0); }\n}\n\n.global-jiggle {\n animation: jiggle 0.4s ease-in-out 2;\n}\n"], 5 | "mappings": "AAAA,MACI,eAAe,QACf,oBAAoB,QACpB,wBAAwB,GAAG,EAAE,GAAG,EAAE,IAClC,sBAAsB,KAAK,IAAI,uBAAuB,EAAE,GAC5D,CAEA,WAAW,OACP,GAAK,UAAW,UAAW,EAAI,CAC/B,IAAM,UAAW,UAAW,KAAO,CACnC,IAAM,UAAW,UAAW,EAAI,CAChC,IAAM,UAAW,UAAW,IAAM,CAClC,GAAO,UAAW,UAAW,EAAI,CACrC,CAEA,CAAC,cACG,UAAW,OAAO,IAAK,YAAY,CACvC", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /frontends/web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CRM 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontends/web/public/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { "source": "**", "destination": "/index.html" } 4 | ] 5 | } -------------------------------------------------------------------------------- /frontends/web/src/core/ToDoItem.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Here is where we define the to-do items for data interaction we need the following 3 | 4 | - [] 5 | */ 6 | import {Operation} from "src/core/interfaces"; 7 | 8 | export default class Todo { 9 | id: number; // SERIAL PRIMARY KEY 10 | name: string; // VARCHAR NOT NULL 11 | dueDate?: Date | null; // TIMESTAMP (optional) 12 | assignedBy: number; // INTEGER NOT NULL (FK users.id) 13 | assignedTo: number; // INTEGER NOT NULL (FK users.id) 14 | description?: string | null; // TEXT (optional) 15 | dateAssigned: Date; // TIMESTAMP NOT NULL DEFAULT NOW() 16 | dateFinished?: Date | null; // TIMESTAMP (optional) 17 | finished: boolean; // BOOLEAN NOT NULL DEFAULT FALSE 18 | handleTrigger: ((item: Todo, operation: Operation) => Promise) | null; 19 | 20 | constructor(params: { 21 | id: number; 22 | name: string; 23 | assignedBy: number; 24 | assignedTo: number; 25 | dateAssigned?: Date; 26 | dueDate?: Date | null; 27 | description?: string | null; 28 | dateFinished?: Date | null; 29 | finished?: boolean; 30 | }) { 31 | this.id = params.id; 32 | this.name = params.name; 33 | this.assignedBy = params.assignedBy; 34 | this.assignedTo = params.assignedTo; 35 | this.description = params.description ?? null; 36 | this.dueDate = params.dueDate ?? null; 37 | this.dateAssigned = params.dateAssigned ?? new Date(); 38 | this.dateFinished = params.dateFinished ?? null; 39 | this.finished = params.finished ?? false; 40 | this.handleTrigger = null; 41 | } 42 | 43 | async performOperation(operation: Operation) { 44 | if (this.handleTrigger) { 45 | await this.handleTrigger(this, operation); 46 | } 47 | } 48 | } 49 | 50 | 51 | export class NewTodo { 52 | name: string; // String 53 | dueDate?: Date | null; // Option 54 | assignedBy: number; // i32 55 | assignedTo: number; // i32 56 | description?: string | null; // Option 57 | dateAssigned?: Date | null; // Option 58 | 59 | constructor(params: { 60 | name: string; 61 | assignedBy: number; 62 | assignedTo: number; 63 | dueDate?: Date | null; 64 | description?: string | null; 65 | dateAssigned?: Date | null; 66 | }) { 67 | this.name = params.name; 68 | this.assignedBy = params.assignedBy; 69 | this.assignedTo = params.assignedTo; 70 | this.dueDate = params.dueDate ?? null; 71 | this.description = params.description ?? null; 72 | this.dateAssigned = params.dateAssigned ?? null; 73 | } 74 | 75 | assignDate(date: Date = new Date()) { 76 | this.dateAssigned = date; 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /frontends/web/src/core/__tests__/ToDoItem.test.ts: -------------------------------------------------------------------------------- 1 | import { Todo } from '../ToDoItem'; // Update the path as needed 2 | 3 | describe('Todo Class', () => { 4 | 5 | it('should create a todo with required fields', () => { 6 | const todo = new Todo({ 7 | id: 1, 8 | name: 'Write Jest tests', 9 | assignedBy: 10, 10 | assignedTo: 20 11 | }); 12 | 13 | expect(todo.id).toBe(1); 14 | expect(todo.name).toBe('Write Jest tests'); 15 | expect(todo.assignedBy).toBe(10); 16 | expect(todo.assignedTo).toBe(20); 17 | 18 | // Check default values 19 | expect(todo.finished).toBe(false); 20 | expect(todo.dateFinished).toBeNull(); 21 | expect(todo.description).toBeNull(); 22 | expect(todo.dueDate).toBeNull(); 23 | expect(todo.dateAssigned).toBeInstanceOf(Date); 24 | }); 25 | 26 | it('should mark the todo as finished', () => { 27 | const todo = new Todo({ 28 | id: 2, 29 | name: 'Complete testing', 30 | assignedBy: 11, 31 | assignedTo: 22 32 | }); 33 | 34 | // Initially unfinished 35 | expect(todo.finished).toBe(false); 36 | expect(todo.dateFinished).toBeNull(); 37 | 38 | todo.markAsFinished(); 39 | 40 | expect(todo.finished).toBe(true); 41 | expect(todo.dateFinished).toBeInstanceOf(Date); 42 | }); 43 | 44 | it('should assign the todo to another user and reset finish state', () => { 45 | const todo = new Todo({ 46 | id: 3, 47 | name: 'Review code', 48 | assignedBy: 12, 49 | assignedTo: 25, 50 | finished: true, 51 | dateFinished: new Date() 52 | }); 53 | 54 | todo.assignTo(30); 55 | 56 | expect(todo.assignedTo).toBe(30); 57 | expect(todo.finished).toBe(false); 58 | expect(todo.dateFinished).toBeNull(); 59 | expect(todo.dateAssigned).toBeInstanceOf(Date); 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /frontends/web/src/core/__tests__/basic.test.ts: -------------------------------------------------------------------------------- 1 | export function add(a: number, b: number): number { 2 | return a + b; 3 | } 4 | 5 | export function multiply(a: number, b: number): number { 6 | return a * b; 7 | } 8 | 9 | 10 | describe('Math functions', () => { 11 | test('adds two numbers', () => { 12 | expect(add(2, 3)).toBe(5); 13 | }); 14 | 15 | test('multiplies two numbers', () => { 16 | expect(multiply(4, 5)).toBe(20); 17 | }); 18 | }); -------------------------------------------------------------------------------- /frontends/web/src/core/__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | // async function reactComponent(item: ToDo, date: Date) { 5 | // 6 | // fire() { 7 | // item.performOperation({type: "Complete", cutOffDate: date}) 8 | // } 9 | // return 10 | // <> 11 | // <{item.name}> 12 | // <{item.assignedBy}> 13 | // <{item.description}> 14 | // 15 | // <> 16 | // } 17 | -------------------------------------------------------------------------------- /frontends/web/src/core/httpProcesses.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxwellflitton/web-server-example/c33fef57775a913fc0df66d13732bc7ad726bbe4/frontends/web/src/core/httpProcesses.ts -------------------------------------------------------------------------------- /frontends/web/src/core/interfaces.ts: -------------------------------------------------------------------------------- 1 | import ToDoItem from "src/core/ToDoItem"; 2 | import NewTodo from "src/core/ToDoItem"; 3 | import { ToDoResponse } from "./utils"; 4 | 5 | export interface HasJWT { 6 | jwt: string; 7 | } 8 | 9 | export interface ItemGetter extends HasJWT { 10 | getItems(jwt: string, userId: number, cutOffDate: Date): Promise; 11 | } 12 | 13 | export interface CompleteItemProcess extends HasJWT { 14 | completeItem(jwt: string, userId: number, toDoId: number, cutOffDate: Date): Promise; 15 | } 16 | 17 | export interface DeleteItemProcess extends HasJWT { 18 | deleteItem(jwt: string, userId: number, toDoId: number, cutOffDate: Date): Promise; 19 | } 20 | 21 | export interface CreateItemProcess extends HasJWT { 22 | createItem(jwt: string, userId: number, item: NewTodo, cutOffDate: Date): Promise; 23 | } 24 | 25 | export interface ReassignItemProcess extends HasJWT { 26 | reassignItem(jwt: string, userId: number, toDoId: number, newUserId: number, cutOffDate: Date): Promise; 27 | } 28 | 29 | export type FullItemProcess = ItemGetter & 30 | CompleteItemProcess & 31 | DeleteItemProcess & 32 | CreateItemProcess & 33 | ReassignItemProcess; 34 | 35 | export type Operation = 36 | | {type: "Complete", cutOffDate: Date} 37 | | {type: "Create", cutOffDate: Date} 38 | | {type: "Delete", cutOffDate: Date} 39 | | {type: "Reassign", newId: number, cutOffDate: Date}; 40 | 41 | -------------------------------------------------------------------------------- /frontends/web/src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import Todo from "./ToDoItem"; 2 | 3 | 4 | type CustomErrorOptions = 5 | | { code: 1; message: string } 6 | | { code: 0; message: null }; 7 | 8 | type ToDoResponseOptions = 9 | | { errorMessage: string; toDos: null } 10 | | { errorMessage: null; toDos: Todo[] }; 11 | 12 | 13 | export class EmptyResponse { 14 | code: number; 15 | message: string | null; 16 | 17 | constructor(options: CustomErrorOptions) { 18 | this.code = options.code; 19 | this.message = options.message; 20 | } 21 | } 22 | 23 | export class ToDoResponse { 24 | errorMessage: string | null; 25 | toDos: Todo[] | null; 26 | 27 | constructor(options: ToDoResponseOptions) { 28 | this.errorMessage = options.errorMessage; 29 | this.toDos = options.toDos; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontends/web/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-white: #ffffff; 3 | --color-light-grey: #f5f5f5; 4 | --color-light-grey-rgb: 245, 245, 245; 5 | --overlay-light-grey: rgba(var(--color-light-grey-rgb), 0.6); 6 | } 7 | 8 | @keyframes jiggle { 9 | 0% { transform: translateX(0); } 10 | 25% { transform: translateX(-5px); } 11 | 50% { transform: translateX(0); } 12 | 75% { transform: translateX(5px); } 13 | 100% { transform: translateX(0); } 14 | } 15 | 16 | .global-jiggle { 17 | animation: jiggle 0.4s ease-in-out 2; 18 | } 19 | -------------------------------------------------------------------------------- /frontends/web/src/index.tsx: -------------------------------------------------------------------------------- 1 | // File: frontend/src/index.tsx 2 | import React from "react"; 3 | import ReactDOM from "react-dom/client"; 4 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 5 | import { AuthPageRoutes } from "./ui/pages/AuthPages/routes"; 6 | import "./index.css"; 7 | 8 | /** 9 | * Similar to the "compileRoutes" pattern, we just return an array of route objects. 10 | * Each object can have `path`, `element`, children, etc. 11 | */ 12 | function getAllRoutes() { 13 | const baseRoutes = [ 14 | { 15 | path: "*", 16 | element:

404

, 17 | }, 18 | ]; 19 | 20 | // Merge your AuthPageRoutes with baseRoutes 21 | return [...AuthPageRoutes, ...baseRoutes]; 22 | } 23 | 24 | // Create the router from your compiled routes 25 | const router = createBrowserRouter(getAllRoutes()); 26 | 27 | // Render the router inside 28 | const rootElement = document.getElementById("root"); 29 | if (rootElement) { 30 | const root = ReactDOM.createRoot(rootElement); 31 | root.render(); 32 | } 33 | 34 | // If you need to export a component for Storybook: 35 | export const App = () => ; 36 | export default App; 37 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/auth/login.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * login.mock.ts 3 | * 4 | * Provides a mock version of the `login` API function for testing purposes. 5 | * This module re-exports all exports from the original `login` module and replaces 6 | * the `login` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/auth/login'; 11 | import type { LoginFunction } from 'api-modules/serverApi/auth/auth/login'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/auth/login'; 15 | 16 | /** 17 | * Mock implementation of the `login` function. 18 | * 19 | * This function wraps the actual `login` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const loginUser = fn(actual.loginUser).mockName('login'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/auth/requestPasswordReset.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * requestPasswordReset.mock.ts 3 | * 4 | * Provides a mock version of the `requestPasswordReset` API function for testing purposes. 5 | * This module re-exports all exports from the original `requestPasswordReset` module and replaces 6 | * the `requestPasswordReset` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/auth/requestPasswordReset'; 11 | import type { RequestPasswordResetFunction } from 'api-modules/serverApi/auth/auth/requestPasswordReset'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/auth/requestPasswordReset'; 15 | 16 | /** 17 | * Mock implementation of the `requestPasswordReset` function. 18 | * 19 | * This function wraps the actual `requestPasswordReset` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const requestPasswordReset = fn(actual.requestPasswordReset).mockName('requestPasswordReset'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/auth/resendConfirmationEmail.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * resendConfirmationEmail.mock.ts 3 | * 4 | * Provides a mock version of the `resendConfirmationEmail` API function for testing purposes. 5 | * This module re-exports all exports from the original `resendConfirmationEmail` module and replaces 6 | * the `resendConfirmationEmail` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/auth/resendConfirmationEmail'; 11 | import type { ResendConfirmationEmailFunction } from 'api-modules/serverApi/auth/auth/resendConfirmationEmail'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/auth/resendConfirmationEmail'; 15 | 16 | /** 17 | * Mock implementation of the `resendConfirmationEmail` function. 18 | * 19 | * This function wraps the actual `resendConfirmationEmail` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const resendConfirmationEmail = fn(actual.resendConfirmationEmail).mockName('resendConfirmationEmail'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/localStorage/localStorage.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * localStorage.mock.ts 3 | * 4 | * Provides a mock version of the `localStorage` utilities (`getJwt`, `setJwt`). 5 | * This module re-exports everything from the real localStorage file, and wraps the 6 | * key functions in Storybook's `fn`, so you can mockReturnValue or mockResolvedValue, etc. 7 | */ 8 | 9 | import { fn } from "@storybook/test"; 10 | // If your real localStorage file is located at `api-modules/auth/localStorage.ts`: 11 | import * as actual from "api-modules/auth/localStorage"; 12 | 13 | // Re-export everything so you still have access to other exports if needed 14 | export * from "api-modules/auth/localStorage"; 15 | 16 | // Overwrite the specific functions you want to mock 17 | export const setJwt = fn(actual.setJwt).mockName("setJwt"); 18 | export const getJwt = fn(actual.getJwt).mockName("getJwt"); 19 | export const setRole = fn(actual.setRole).mockName("setRole"); 20 | export const getRole = fn(actual.getRole).mockName("getRole"); 21 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/roles/assignRole.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * assignRole.mock.ts 3 | * 4 | * Provides a mock version of the `assignRole` API function for testing purposes. 5 | * This module re-exports all exports from the original `assignRole` module and replaces 6 | * the `assignRole` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/roles/assignRole'; 11 | import type { AssignRoleFunction } from 'api-modules/serverApi/auth/roles/assignRole'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/roles/assignRole'; 15 | 16 | /** 17 | * Mock implementation of the `assignRole` function. 18 | * 19 | * This function wraps the actual `assignRole` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const assignRole = fn(actual.assignRole).mockName('assignRole'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/roles/removeRole.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * removeRole.mock.ts 3 | * 4 | * Provides a mock version of the `removeRole` API function for testing purposes. 5 | * This module re-exports all exports from the original `removeRole` module and replaces 6 | * the `removeRole` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/roles/removeRole'; 11 | import type { RemoveRoleFunction } from 'api-modules/serverApi/auth/roles/removeRole'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/roles/removeRole'; 15 | 16 | /** 17 | * Mock implementation of the `removeRole` function. 18 | * 19 | * This function wraps the actual `removeRole` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const removeRole = fn(actual.removeRole).mockName('removeRole'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/roles/updateRoles.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * updateRoles.mock.ts 3 | * 4 | * Provides a mock version of the `updateRoles` API function for testing purposes. 5 | * This module re-exports all exports from the original `updateRoles` module and replaces 6 | * the `updateRoles` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/roles/updateRoles'; 11 | import type { UpdateRolesFunction } from 'api-modules/serverApi/auth/roles/updateRoles'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/roles/updateRoles'; 15 | 16 | /** 17 | * Mock implementation of the `updateRoles` function. 18 | * 19 | * This function wraps the actual `updateRoles` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const updateRoles = fn(actual.updateRoles).mockName('updateRoles'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/users/blockUser.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * blockUser.mock.ts 3 | * 4 | * Provides a mock version of the `blockUser` API function for testing purposes. 5 | * This module re-exports all exports from the original `blockUser` module and replaces 6 | * the `blockUser` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/users/blockUser'; 11 | import type { BlockUserFunction } from 'api-modules/serverApi/auth/users/blockUser'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/users/blockUser'; 15 | 16 | /** 17 | * Mock implementation of the `blockUser` function. 18 | * 19 | * This function wraps the actual `blockUser` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const blockUser = fn(actual.blockUser).mockName('blockUser'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/users/confirmUser.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * confirmUser.mock.ts 3 | * 4 | * Provides a mock version of the `confirmUser` API function for testing purposes. 5 | * This module re-exports all exports from the original `confirmUser` module and replaces 6 | * the `confirmUser` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/users/confirmUser'; 11 | import type { ConfirmUserFunction } from 'api-modules/serverApi/auth/users/confirmUser'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/users/confirmUser'; 15 | 16 | /** 17 | * Mock implementation of the `confirmUser` function. 18 | * 19 | * This function wraps the actual `confirmUser` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const confirmUser = fn(actual.confirmUser).mockName('confirmUser'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/users/createSuperUser.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * createSuperUser.mock.ts 3 | * 4 | * Provides a mock version of the `createSuperUser` API function for testing purposes. 5 | * This module re-exports all exports from the original `createUser` module and replaces 6 | * the `createSuperUser` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/users/createSuperUser'; 11 | import type { CreateSuperUserFunction } from 'api-modules/serverApi/auth/users/createSuperUser'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/users/createSuperUser'; 15 | 16 | /** 17 | * Mock implementation of the `createSuperUser` function. 18 | * 19 | * This function wraps the actual `createSuperUser` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const createSuperUser = fn(actual.createSuperUser).mockName('createSuperUser'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/users/createUser.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * createUser.mock.ts 3 | * 4 | * Provides a mock version of the `createUser` API function for testing purposes. 5 | * This module re-exports all exports from the original `createUser` module and replaces 6 | * the `createUser` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/users/createUser'; 11 | import type { CreateUserFunction } from 'api-modules/serverApi/auth/users/createUser'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/users/createUser'; 15 | 16 | /** 17 | * Mock implementation of the `createUser` function. 18 | * 19 | * This function wraps the actual `createUser` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const createUser = fn(actual.createUser).mockName('createUser'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/users/deleteUser.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * deleteUser.mock.ts 3 | * 4 | * Provides a mock version of the `deleteUser` API function for testing purposes. 5 | * This module re-exports all exports from the original `deleteUser` module and replaces 6 | * the `deleteUser` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/users/deleteUser'; 11 | import type { DeleteUserFunction } from 'api-modules/serverApi/auth/users/deleteUser'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/users/deleteUser'; 15 | 16 | /** 17 | * Mock implementation of the `deleteUser` function. 18 | * 19 | * This function wraps the actual `deleteUser` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const deleteUser = fn(actual.deleteUser).mockName('deleteUser'); -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/users/getAllUsers.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * getAllUsers.mock.ts 3 | * 4 | * Provides a mock version of the `getAllUsers` API function for testing purposes. 5 | * This module re-exports all exports from the original `getAllUsers` module and replaces 6 | * the `getAllUsers` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/users/getAllUsers'; 11 | import type { GetAllUsersFunction } from 'api-modules/serverApi/auth/users/getAllUsers'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/users/getAllUsers'; 15 | 16 | /** 17 | * Mock implementation of the `getAllUsers` function. 18 | * 19 | * This function wraps the actual `getAllUsers` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const getAllUsers = fn(actual.getAllUsers).mockName('getAllUsers'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/users/getUser.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * getUser.mock.ts 3 | * 4 | * Provides a mock version of the `getUser` API function for testing purposes. 5 | * This module re-exports all exports from the original `getUser` module and replaces 6 | * the `getUser` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/users/getUser'; 11 | import type { GetUserFunction } from 'api-modules/serverApi/auth/users/getUser'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/users/getUser'; 15 | 16 | /** 17 | * Mock implementation of the `getUser` function. 18 | * 19 | * This function wraps the actual `getUser` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const getUser = fn(actual.getUser).mockName('getUser'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/users/resetPassword.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * resetPassword.mock.ts 3 | * 4 | * Provides a mock version of the `resetPassword` API function for testing purposes. 5 | * This module re-exports all exports from the original `resetPassword` module and replaces 6 | * the `resetPassword` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/users/resetPassword'; 11 | import type { ResetPasswordFunction } from 'api-modules/serverApi/auth/users/resetPassword'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/users/resetPassword'; 15 | 16 | /** 17 | * Mock implementation of the `resetPassword` function. 18 | * 19 | * This function wraps the actual `resetPassword` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const resetPassword = fn(actual.resetPassword).mockName('resetPassword'); 23 | -------------------------------------------------------------------------------- /frontends/web/src/mockApis/auth/users/unblockUser.mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * unblockUser.mock.ts 3 | * 4 | * Provides a mock version of the `unblockUser` API function for testing purposes. 5 | * This module re-exports all exports from the original `unblockUser` module and replaces 6 | * the `unblockUser` function with a mock implementation using Storybook's `fn`. 7 | */ 8 | 9 | import { fn } from '@storybook/test'; 10 | import * as actual from 'api-modules/serverApi/auth/users/unblockUser'; 11 | import type { UnblockUserFunction } from 'api-modules/serverApi/auth/users/unblockUser'; 12 | 13 | // Re-export everything from the original module 14 | export * from 'api-modules/serverApi/auth/users/unblockUser'; 15 | 16 | /** 17 | * Mock implementation of the `unblockUser` function. 18 | * 19 | * This function wraps the actual `unblockUser` implementation, 20 | * allowing tests to simulate API behavior without making real HTTP requests. 21 | */ 22 | export const unblockUser = fn(actual.unblockUser).mockName('unblockUser'); -------------------------------------------------------------------------------- /frontends/web/src/ui/components/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import React from "react"; 3 | import { passThroughRef } from "src/ui/helpers/passThroughRef"; 4 | import type { Interpolation, Theme } from "@emotion/react"; 5 | 6 | type ButtonProps = React.JSX.IntrinsicElements["button"] & { 7 | id?: string; 8 | css?: Interpolation; 9 | }; 10 | 11 | function _Button(props: ButtonProps) { 12 | return ( 13 |