├── signature
├── src
│ └── lib.rs
└── Cargo.toml
├── ui
├── .npmrc
├── client
│ ├── src
│ │ ├── mocks
│ │ │ ├── server.ts
│ │ │ ├── stub-new-work
│ │ │ │ ├── index.ts
│ │ │ │ └── projects.ts
│ │ │ ├── browser.ts
│ │ │ ├── config.ts
│ │ │ └── config.test.ts
│ │ ├── app
│ │ │ ├── pages
│ │ │ │ ├── home
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── home.tsx
│ │ │ │ ├── documents
│ │ │ │ │ └── index.ts
│ │ │ │ ├── project-list
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── components
│ │ │ │ │ │ └── project-form.tsx
│ │ │ │ └── project-details
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── project-details.tsx
│ │ │ ├── react-app-env.d.ts
│ │ │ ├── layout
│ │ │ │ ├── index.ts
│ │ │ │ ├── layout-constants.ts
│ │ │ │ ├── default-layout.tsx
│ │ │ │ ├── sidebar.tsx
│ │ │ │ └── about.tsx
│ │ │ ├── axios-config
│ │ │ │ ├── index.ts
│ │ │ │ └── apiInit.ts
│ │ │ ├── hooks
│ │ │ │ ├── table-controls
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── getPaginationHubRequestParams.ts
│ │ │ │ │ ├── getHubRequestParams.ts
│ │ │ │ │ └── getSortHubRequestParams.ts
│ │ │ │ ├── useCreateEditModalState.ts
│ │ │ │ ├── useStorage.ts
│ │ │ │ └── useSelectionState.ts
│ │ │ ├── env.ts
│ │ │ ├── components
│ │ │ │ ├── HookFormPFFields
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── HookFormPFTextArea.tsx
│ │ │ │ │ ├── HookFormPFSelect.tsx
│ │ │ │ │ ├── HookFormPFTextInput.tsx
│ │ │ │ │ └── HookFormPFGroupController.tsx
│ │ │ │ ├── StateNoData.tsx
│ │ │ │ ├── AppPlaceholder.tsx
│ │ │ │ ├── ConditionalTooltip.tsx
│ │ │ │ ├── StateNoResults.tsx
│ │ │ │ ├── NoDataEmptyState.tsx
│ │ │ │ ├── KeyDisplayToggle.tsx
│ │ │ │ ├── StateError.tsx
│ │ │ │ ├── Notifications.tsx
│ │ │ │ ├── AppTableActionButtons.tsx
│ │ │ │ ├── OidcProvider.tsx
│ │ │ │ ├── NotificationsContext.tsx
│ │ │ │ ├── ConfirmDialog.tsx
│ │ │ │ └── ToolbarBulkSelector.tsx
│ │ │ ├── setupTests.ts
│ │ │ ├── Constants.ts
│ │ │ ├── utils
│ │ │ │ ├── utils.test.ts
│ │ │ │ ├── type-utils.ts
│ │ │ │ └── utils.ts
│ │ │ ├── i18n.ts
│ │ │ ├── reportWebVitals.ts
│ │ │ ├── App.css
│ │ │ ├── App.tsx
│ │ │ ├── images
│ │ │ │ ├── pfbg-icon.svg
│ │ │ │ └── avatar.svg
│ │ │ ├── oidc.ts
│ │ │ ├── queries
│ │ │ │ ├── ubl-documents.ts
│ │ │ │ ├── projects.ts
│ │ │ │ └── credentials.ts
│ │ │ ├── context
│ │ │ │ └── ProjectContext.tsx
│ │ │ ├── Routes.tsx
│ │ │ └── api
│ │ │ │ ├── models.ts
│ │ │ │ └── rest.ts
│ │ └── index.tsx
│ ├── public
│ │ ├── robots.txt
│ │ ├── openubl-favicon.ico
│ │ ├── manifest.json
│ │ ├── index.html.ejs
│ │ └── locales
│ │ │ ├── en
│ │ │ └── translation.json
│ │ │ └── es
│ │ │ └── translation.json
│ ├── types
│ │ ├── globals.d.ts
│ │ ├── typings.d.ts
│ │ ├── @hookform_resolvers_2.9.11.d.ts
│ │ └── array-filter-Boolean.ts
│ ├── config
│ │ ├── monacoConstants.ts
│ │ ├── stylePaths.js
│ │ ├── jest.config.ts
│ │ ├── webpack.prod.ts
│ │ └── webpack.dev.ts
│ ├── i18next-parser.config.js
│ ├── tsconfig.json
│ └── package.json
├── .dockerignore
├── README.md
├── .prettierignore
├── .editorconfig
├── .prettierrc.mjs
├── common
│ ├── tsconfig.json
│ ├── rollup.config.js
│ ├── src
│ │ ├── index.ts
│ │ ├── proxies.ts
│ │ └── environment.ts
│ └── package.json
├── .gitignore
├── server
│ ├── rollup.config.js
│ ├── package.json
│ └── src
│ │ └── index.js
├── entrypoint.sh
├── Dockerfile
├── scripts
│ └── verify_lock.mjs
├── .eslintrc.cjs
└── package.json
├── common
├── src
│ ├── lib.rs
│ └── config.rs
└── Cargo.toml
├── api
├── src
│ ├── lib.rs
│ ├── system
│ │ ├── error.rs
│ │ ├── mod.rs
│ │ └── credentials.rs
│ └── db.rs
└── Cargo.toml
├── entity
├── src
│ ├── prelude.rs
│ ├── mod.rs
│ ├── lib.rs
│ ├── document.rs
│ ├── keystore.rs
│ ├── send_rule.rs
│ ├── keystore_config.rs
│ ├── credentials.rs
│ └── delivery.rs
└── Cargo.toml
├── .devcontainer
├── postCreateCommand.sh
├── Dockerfile
└── devcontainer.json
├── migration
├── src
│ ├── main.rs
│ ├── lib.rs
│ ├── m20240113_213636_create_keystore.rs
│ ├── m20240117_142858_create_send_rule.rs
│ ├── m20240101_104121_create_document.rs
│ ├── m20240113_213657_create_keystore_config.rs
│ ├── m20240114_154538_create_credentials.rs
│ └── m20240717_214515_create_delivery.rs
├── Cargo.toml
└── README.md
├── deploy
└── compose
│ ├── scripts
│ ├── minio
│ │ └── setup.sh
│ └── keycloak
│ │ └── setup.sh
│ └── compose.yaml
├── README.md
├── cli
├── Cargo.toml
└── src
│ └── main.rs
├── server
├── src
│ ├── server
│ │ ├── health.rs
│ │ ├── mod.rs
│ │ └── credentials.rs
│ ├── lib.rs
│ └── dto.rs
└── Cargo.toml
├── storage
├── Cargo.toml
└── src
│ └── config.rs
├── .gitignore
└── Cargo.toml
/signature/src/lib.rs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ui/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/ui/client/src/mocks/server.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/common/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod config;
2 |
--------------------------------------------------------------------------------
/ui/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | */dist/
3 |
--------------------------------------------------------------------------------
/api/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod db;
2 | pub mod system;
3 |
--------------------------------------------------------------------------------
/ui/README.md:
--------------------------------------------------------------------------------
1 | ## Spog UI
2 |
3 | export AUTH_REQUIRED=true
--------------------------------------------------------------------------------
/entity/src/prelude.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
2 |
--------------------------------------------------------------------------------
/ui/client/src/app/pages/home/index.ts:
--------------------------------------------------------------------------------
1 | export { Home as default } from "./home";
2 |
--------------------------------------------------------------------------------
/ui/client/src/app/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/ui/client/src/app/layout/index.ts:
--------------------------------------------------------------------------------
1 | export { DefaultLayout } from "./default-layout";
2 |
--------------------------------------------------------------------------------
/ui/client/src/app/axios-config/index.ts:
--------------------------------------------------------------------------------
1 | export { initInterceptors } from "./apiInit";
2 |
--------------------------------------------------------------------------------
/ui/client/src/app/hooks/table-controls/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./getHubRequestParams";
2 |
--------------------------------------------------------------------------------
/ui/client/src/app/pages/documents/index.ts:
--------------------------------------------------------------------------------
1 | export { Projects as default } from "./documents";
2 |
--------------------------------------------------------------------------------
/entity/src/mod.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
2 |
3 | pub mod prelude;
4 |
--------------------------------------------------------------------------------
/ui/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/ui/client/src/app/pages/project-list/index.ts:
--------------------------------------------------------------------------------
1 | export { ProjectList as default } from "./project-list";
2 |
--------------------------------------------------------------------------------
/ui/client/src/app/pages/project-details/index.ts:
--------------------------------------------------------------------------------
1 | export { ProjectDetails as default } from "./project-details";
2 |
--------------------------------------------------------------------------------
/.devcontainer/postCreateCommand.sh:
--------------------------------------------------------------------------------
1 | ## Git autocomplete
2 | echo "source /usr/share/bash-completion/completions/git" >> ~/.bashrc
3 |
--------------------------------------------------------------------------------
/ui/client/public/openubl-favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/project-openubl/ublhub/HEAD/ui/client/public/openubl-favicon.ico
--------------------------------------------------------------------------------
/ui/client/src/app/layout/layout-constants.ts:
--------------------------------------------------------------------------------
1 | type ThemeType = "light" | "dark";
2 | export const LayoutTheme: ThemeType = "dark";
3 |
--------------------------------------------------------------------------------
/migration/src/main.rs:
--------------------------------------------------------------------------------
1 | use sea_orm_migration::prelude::*;
2 |
3 | #[async_std::main]
4 | async fn main() {
5 | cli::run_cli(migration::Migrator).await;
6 | }
7 |
--------------------------------------------------------------------------------
/common/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "openubl-common"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | clap = { workspace = true, features = ["derive", "env"] }
8 |
--------------------------------------------------------------------------------
/ui/client/src/app/env.ts:
--------------------------------------------------------------------------------
1 | import { decodeEnv, buildOpenublEnv } from "@openubl-ui/common";
2 |
3 | export const ENV = buildOpenublEnv(decodeEnv(window._env));
4 |
5 | export default ENV;
6 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/devcontainers/rust:bullseye
2 | RUN apt-get update && apt-get -y install pkg-config xmlsec1 libxml2-dev libxmlsec1-dev libxmlsec1-openssl libclang-dev
3 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/HookFormPFFields/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./HookFormPFGroupController";
2 | export * from "./HookFormPFTextInput";
3 | export * from "./HookFormPFTextArea";
4 | export * from "./HookFormPFSelect";
5 |
--------------------------------------------------------------------------------
/ui/client/types/globals.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | declare global {
4 | interface Window {
5 | /**
6 | * base64 encoded JS object containing any environment configurations.
7 | */
8 | _env: string;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/entity/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
2 |
3 | pub mod prelude;
4 |
5 | pub mod credentials;
6 | pub mod delivery;
7 | pub mod document;
8 | pub mod keystore;
9 | pub mod keystore_config;
10 | pub mod send_rule;
11 |
--------------------------------------------------------------------------------
/ui/.prettierignore:
--------------------------------------------------------------------------------
1 | # Library, IDE and build locations
2 | **/node_modules/
3 | **/coverage/
4 | **/dist/
5 | .vscode/
6 | .idea/
7 | .eslintcache/
8 |
9 | #
10 | # NOTE: Could ignore anything that eslint will look at since eslint also applies
11 | # prettier.
12 | #
13 |
--------------------------------------------------------------------------------
/ui/client/src/app/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom";
6 |
--------------------------------------------------------------------------------
/entity/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "openubl-entity"
3 | version = "0.1.0"
4 | edition = "2021"
5 | publish = false
6 |
7 | [dependencies]
8 | sea-orm = { workspace = true, features = [
9 | "sqlx-sqlite",
10 | "sqlx-postgres",
11 | "runtime-tokio-rustls",
12 | "macros",
13 | ] }
14 |
--------------------------------------------------------------------------------
/ui/.editorconfig:
--------------------------------------------------------------------------------
1 | root=true
2 |
3 | [*]
4 | # standard prettier behaviors
5 | charset = utf-8
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 |
9 | # Configurable prettier behaviors
10 | end_of_line = lf
11 | indent_style = space
12 | indent_size = 2
13 | max_line_length = 80
14 |
--------------------------------------------------------------------------------
/signature/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "openubl-signature"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | anyhow = { workspace = true }
10 | thiserror = { workspace = true }
11 |
--------------------------------------------------------------------------------
/api/src/system/error.rs:
--------------------------------------------------------------------------------
1 | use sea_orm::DbErr;
2 |
3 | #[derive(Debug, thiserror::Error)]
4 | pub enum Error {
5 | #[error(transparent)]
6 | Json(#[from] serde_json::Error),
7 |
8 | #[error(transparent)]
9 | Database(#[from] DbErr),
10 |
11 | #[error(transparent)]
12 | Any(#[from] anyhow::Error),
13 | }
14 |
--------------------------------------------------------------------------------
/ui/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "openubl-ui",
3 | "name": "Openubl UI",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/deploy/compose/scripts/minio/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -l
2 |
3 | # Connect to minio
4 | /usr/bin/mc config host add myminio http://minio:9000 admin password;
5 |
6 | # Create buckets
7 | /usr/bin/mc mb myminio/openubl || true;
8 |
9 | # Config events
10 | /usr/bin/mc event add myminio/openubl arn:minio:sqs::OPENUBL:nats --event "put,delete";
11 |
12 | # Restart service
13 | /usr/bin/mc admin service restart myminio;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## dev-env
2 |
3 | Starting:
4 |
5 | ```shell
6 | docker-compose -f openubl/deploy/compose/compose.yaml up
7 | ```
8 |
9 | ```shell
10 | RUST_LOG=info cargo watch -x 'run -p openubl-cli -- server --db-user user --db-password password --oidc-auth-server-url http://localhost:9001/realms/openubl minio --storage-minio-host http://localhost:9002 --storage-minio-access-key admin --storage-minio-secret-key password'
11 | ```
12 |
--------------------------------------------------------------------------------
/ui/.prettierrc.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import("prettier").Config} */
2 | const config = {
3 | trailingComma: "es5", // es5 was the default in prettier v2
4 | semi: true,
5 | singleQuote: false,
6 |
7 | // Values used from .editorconfig:
8 | // - printWidth == max_line_length
9 | // - tabWidth == indent_size
10 | // - useTabs == indent_style
11 | // - endOfLine == end_of_line
12 | };
13 |
14 | export default config;
15 |
--------------------------------------------------------------------------------
/ui/common/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 |
4 | "include": ["src/**/*"],
5 |
6 | "compilerOptions": {
7 | "strict": true,
8 | "target": "es2020",
9 | "module": "Node16",
10 | "moduleResolution": "Node16",
11 |
12 | "outDir": "./dist",
13 | "declaration": true,
14 | "declarationMap": true,
15 | "sourceMap": true,
16 | "inlineSources": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/StateNoData.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useTranslation } from "react-i18next";
3 | import { NoDataEmptyState } from "./NoDataEmptyState";
4 |
5 | export const StateNoData: React.FC = () => {
6 | const { t } = useTranslation();
7 |
8 | return (
9 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/cli/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "openubl-cli"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | openubl-server = { workspace = true }
10 |
11 | clap = { workspace = true, features = ["derive", "env"] }
12 | anyhow = { workspace = true }
13 | actix-web = { workspace = true }
14 | log = { workspace = true }
15 | tokio = { workspace = true, features = ["full"] }
16 |
--------------------------------------------------------------------------------
/ui/client/src/app/Constants.ts:
--------------------------------------------------------------------------------
1 | import ENV from "./env";
2 |
3 | export enum BrandType {
4 | Openubl = "openubl",
5 | LF = "lf",
6 | }
7 | export const APP_BRAND = ENV.PROFILE as BrandType;
8 |
9 | // URL param prefixes: should be short, must be unique for each table that uses one
10 | export enum TableURLParamKeyPrefix {
11 | repositories = "r",
12 | tags = "t",
13 | }
14 |
15 | export const isAuthRequired = ENV.AUTH_REQUIRED !== "false";
16 | export const uploadLimit = "500m";
17 |
--------------------------------------------------------------------------------
/ui/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules/
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | coverage/
10 |
11 | # production
12 | dist/
13 | /qa/build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 |
24 | .eslintcache
25 |
26 | # VSCode
27 | .vscode/*
28 |
29 | # Intellij IDEA
30 | .idea/
31 |
--------------------------------------------------------------------------------
/ui/client/src/app/utils/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { getToolbarChipKey } from "./utils";
2 |
3 | describe("utils", () => {
4 | // getToolbarChipKey
5 |
6 | it("getToolbarChipKey: test 'string'", () => {
7 | const result = getToolbarChipKey("myValue");
8 | expect(result).toBe("myValue");
9 | });
10 |
11 | it("getToolbarChipKey: test 'ToolbarChip'", () => {
12 | const result = getToolbarChipKey({ key: "myKey", node: "myNode" });
13 | expect(result).toBe("myKey");
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/ui/client/src/app/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 | import { initReactI18next } from "react-i18next";
3 | import Backend from "i18next-http-backend";
4 |
5 | i18n
6 | .use(Backend)
7 | .use(initReactI18next)
8 | .init({
9 | lng: "en",
10 | fallbackLng: "en",
11 | debug: false,
12 | react: {
13 | useSuspense: false,
14 | },
15 | interpolation: {
16 | escapeValue: false,
17 | },
18 |
19 | returnEmptyString: false,
20 | });
21 |
22 | export default i18n;
23 |
--------------------------------------------------------------------------------
/ui/client/src/app/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from "web-vitals";
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/ui/client/src/app/utils/type-utils.ts:
--------------------------------------------------------------------------------
1 | export type KeyWithValueType = {
2 | [Key in keyof T]-?: T[Key] extends V ? Key : never;
3 | }[keyof T];
4 |
5 | export type DisallowCharacters<
6 | T extends string,
7 | TInvalidCharacter extends string,
8 | > = T extends `${string}${TInvalidCharacter}${string}` ? never : T;
9 |
10 | export type DiscriminatedArgs =
11 | | ({ [key in TBoolDiscriminatorKey]: true } & TArgs)
12 | | { [key in TBoolDiscriminatorKey]?: false };
13 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/AppPlaceholder.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Bullseye, Spinner } from "@patternfly/react-core";
3 |
4 | export const AppPlaceholder: React.FC = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Loading...
13 |
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/ui/client/src/app/hooks/useCreateEditModalState.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type ModalState =
4 | | { mode: "create"; resource: null }
5 | | { mode: "create"; resource: T }
6 | | { mode: "edit"; resource: T }
7 | | null;
8 |
9 | export default function useCreateEditModalState() {
10 | const [modalState, setModalState] = React.useState>(null);
11 | const isModalOpen = modalState !== null;
12 |
13 | return {
14 | modalState,
15 | setModalState,
16 | isModalOpen,
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/ui/client/config/monacoConstants.ts:
--------------------------------------------------------------------------------
1 | import { EditorLanguage } from "monaco-editor/esm/metadata";
2 |
3 | export const LANGUAGES_BY_FILE_EXTENSION = {
4 | java: "java",
5 | go: "go",
6 | xml: "xml",
7 | js: "javascript",
8 | ts: "typescript",
9 | html: "html",
10 | htm: "html",
11 | css: "css",
12 | yaml: "yaml",
13 | yml: "yaml",
14 | json: "json",
15 | md: "markdown",
16 | php: "php",
17 | py: "python",
18 | pl: "perl",
19 | rb: "ruby",
20 | sh: "shell",
21 | bash: "shell",
22 | } as const satisfies Record;
23 |
--------------------------------------------------------------------------------
/ui/client/src/mocks/stub-new-work/index.ts:
--------------------------------------------------------------------------------
1 | import { type RestHandler } from "msw";
2 | import { config } from "../config";
3 |
4 | import projects from "./projects";
5 |
6 | const enableMe = (me: string) =>
7 | config.stub === "*" ||
8 | (Array.isArray(config.stub) ? (config.stub as string[]).includes(me) : false);
9 |
10 | /**
11 | * Return the stub-new-work handlers that are enabled by config.
12 | */
13 | const enabledStubs: RestHandler[] = [
14 | ...(enableMe("projects") ? projects : []),
15 | ].filter(Boolean);
16 |
17 | export default enabledStubs;
18 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/ConditionalTooltip.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Tooltip, TooltipProps } from "@patternfly/react-core";
3 |
4 | export interface IConditionalTooltipProps extends TooltipProps {
5 | isTooltipEnabled: boolean;
6 | children: React.ReactElement;
7 | }
8 |
9 | // TODO: lib-ui candidate
10 | export const ConditionalTooltip: React.FC = ({
11 | isTooltipEnabled,
12 | children,
13 | ...props
14 | }: IConditionalTooltipProps) =>
15 | isTooltipEnabled ? {children} : children;
16 |
--------------------------------------------------------------------------------
/ui/common/rollup.config.js:
--------------------------------------------------------------------------------
1 | import nodeResolve from "@rollup/plugin-node-resolve";
2 | import typescript from "@rollup/plugin-typescript";
3 |
4 | const config = {
5 | input: "src/index.ts",
6 |
7 | output: [
8 | {
9 | file: "dist/index.mjs",
10 | format: "esm",
11 | sourcemap: true,
12 | },
13 | {
14 | file: "dist/index.cjs",
15 | format: "cjs",
16 | sourcemap: true,
17 | },
18 | ],
19 |
20 | watch: {
21 | clearScreen: false,
22 | },
23 |
24 | plugins: [nodeResolve(), typescript()],
25 | };
26 |
27 | export default config;
28 |
--------------------------------------------------------------------------------
/ui/client/config/stylePaths.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | import * as path from "path";
4 |
5 | export const stylePaths = [
6 | // Include our sources
7 | path.resolve(__dirname, "../src"),
8 |
9 | // Include =PF4 paths, even if nested under another package because npm cannot hoist
13 | // a single package to the root node_modules/
14 | /node_modules\/@patternfly\/patternfly/,
15 | /node_modules\/@patternfly\/react-core\/.*\.css/,
16 | /node_modules\/@patternfly\/react-styles/,
17 | ];
18 |
--------------------------------------------------------------------------------
/server/src/server/health.rs:
--------------------------------------------------------------------------------
1 | use actix_web::{get, web, Responder};
2 |
3 | use crate::server::Error;
4 | use crate::AppState;
5 |
6 | #[utoipa::path(
7 | responses(
8 | (status = 200, description = "Liveness"),
9 | ),
10 | )]
11 | #[get("/health/live")]
12 | pub async fn liveness(_: web::Data) -> Result {
13 | Ok("Live")
14 | }
15 |
16 | #[utoipa::path(
17 | responses(
18 | (status = 200, description = "Readiness"),
19 | ),
20 | )]
21 | #[get("/health/read")]
22 | pub async fn readiness(_: web::Data) -> Result {
23 | Ok("Read")
24 | }
25 |
--------------------------------------------------------------------------------
/ui/client/src/app/App.css:
--------------------------------------------------------------------------------
1 | .pf-c-select__toggle:before {
2 | border-top: var(--pf-c-select__toggle--before--BorderTopWidth) solid
3 | var(--pf-c-select__toggle--before--BorderTopColor) !important;
4 | border-right: var(--pf-c-select__toggle--before--BorderRightWidth) solid
5 | var(--pf-c-select__toggle--before--BorderRightColor) !important;
6 | border-bottom: var(--pf-c-select__toggle--before--BorderBottomWidth) solid
7 | var(--pf-c-select__toggle--before--BorderBottomColor) !important;
8 | border-left: var(--pf-c-select__toggle--before--BorderLeftWidth) solid
9 | var(--pf-c-select__toggle--before--BorderLeftColor) !important;
10 | }
11 |
--------------------------------------------------------------------------------
/ui/client/src/app/pages/home/home.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import {
4 | PageSection,
5 | PageSectionVariants,
6 | Text,
7 | TextContent,
8 | } from "@patternfly/react-core";
9 |
10 | export const Home: React.FC = () => {
11 | return (
12 | <>
13 |
14 |
15 | Trusted Content
16 | Fully hosted and managed service
17 |
18 |
19 | content
20 | >
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/ui/client/i18next-parser.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | module.exports = {
4 | createOldCatalogs: true, // Save the \_old files
5 |
6 | indentation: 2, // Indentation of the catalog files
7 |
8 | keepRemoved: true, // Keep keys from the catalog that are no longer in code
9 |
10 | lexers: {
11 | js: ["JsxLexer"],
12 | ts: ["JsxLexer"],
13 | jsx: ["JsxLexer"],
14 | tsx: ["JsxLexer"],
15 |
16 | default: ["JsxLexer"],
17 | },
18 |
19 | locales: ["en", "es"],
20 |
21 | output: "public/locales/$LOCALE/$NAMESPACE.json",
22 |
23 | input: ["src/**/*.{js,jsx,ts,tsx}"],
24 |
25 | sort: true,
26 | verbose: true,
27 | };
28 |
--------------------------------------------------------------------------------
/entity/src/document.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
2 |
3 | use sea_orm::entity::prelude::*;
4 |
5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
6 | #[sea_orm(table_name = "ubl_document")]
7 | pub struct Model {
8 | #[sea_orm(primary_key)]
9 | pub id: i32,
10 | pub file_id: String,
11 | pub supplier_id: String,
12 | pub identifier: String,
13 | pub r#type: String,
14 | pub voided_document_code: Option,
15 | pub digest_value: Option,
16 | pub sha256: String,
17 | }
18 |
19 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
20 | pub enum Relation {}
21 |
22 | impl ActiveModelBehavior for ActiveModel {}
23 |
--------------------------------------------------------------------------------
/ui/client/src/app/App.tsx:
--------------------------------------------------------------------------------
1 | import "./App.css";
2 | import React from "react";
3 | import { BrowserRouter as Router } from "react-router-dom";
4 |
5 | import { DefaultLayout } from "./layout";
6 | import { AppRoutes } from "./Routes";
7 | import { NotificationsProvider } from "./components/NotificationsContext";
8 |
9 | import "@patternfly/patternfly/patternfly.css";
10 | import "@patternfly/patternfly/patternfly-addons.css";
11 |
12 | const App: React.FC = () => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default App;
25 |
--------------------------------------------------------------------------------
/entity/src/keystore.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
2 |
3 | use sea_orm::entity::prelude::*;
4 |
5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
6 | #[sea_orm(table_name = "keystore")]
7 | pub struct Model {
8 | #[sea_orm(primary_key)]
9 | pub id: i32,
10 | pub name: String,
11 | }
12 |
13 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
14 | pub enum Relation {
15 | #[sea_orm(has_many = "super::keystore_config::Entity")]
16 | KeystoreConfig,
17 | }
18 |
19 | impl Related for Entity {
20 | fn to() -> RelationDef {
21 | Relation::KeystoreConfig.def()
22 | }
23 | }
24 |
25 | impl ActiveModelBehavior for ActiveModel {}
26 |
--------------------------------------------------------------------------------
/ui/client/src/app/images/pfbg-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui/common/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./environment.js";
2 | export * from "./proxies.js";
3 |
4 | /**
5 | * Return a base64 encoded JSON string containing the given `env` object.
6 | */
7 | export const encodeEnv = (env: object, exclude?: string[]): string => {
8 | const filtered = exclude
9 | ? Object.fromEntries(
10 | Object.entries(env).filter(([key]) => !exclude.includes(key))
11 | )
12 | : env;
13 |
14 | return btoa(JSON.stringify(filtered));
15 | };
16 |
17 | /**
18 | * Return an objects from a base64 encoded JSON string.
19 | */
20 | export const decodeEnv = (env: string): object =>
21 | !env ? undefined : JSON.parse(atob(env));
22 |
23 | // TODO: Include `index.html.ejs` to `index.html` template file processing...
24 |
--------------------------------------------------------------------------------
/api/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "openubl-api"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | openubl-entity = { workspace = true }
8 | openubl-common = { workspace = true }
9 | openubl-migration = { workspace = true }
10 | openubl-storage = { workspace = true }
11 |
12 | xhandler = { workspace = true }
13 |
14 | sea-orm = { workspace = true, features = [
15 | "sea-query-binder",
16 | "sqlx-sqlite",
17 | "sqlx-postgres",
18 | "runtime-tokio-rustls",
19 | "macros",
20 | ] }
21 | sea-query = { workspace = true }
22 | async-trait = { workspace = true }
23 | anyhow = { workspace = true }
24 | thiserror = { workspace = true }
25 | serde_json = { workspace = true }
26 | serde = { workspace = true, features = ["derive"] }
27 |
--------------------------------------------------------------------------------
/ui/server/rollup.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | import json from "@rollup/plugin-json";
4 | import nodeResolve from "@rollup/plugin-node-resolve";
5 | import commonjs from "@rollup/plugin-commonjs";
6 | import run from "@rollup/plugin-run";
7 |
8 | const buildAndRun = process.env?.ROLLUP_RUN === "true";
9 |
10 | export default {
11 | input: "src/index.js",
12 | output: {
13 | file: "dist/index.js",
14 | format: "esm",
15 | sourcemap: true,
16 | },
17 | watch: {
18 | clearScreen: false,
19 | },
20 |
21 | plugins: [
22 | nodeResolve({
23 | preferBuiltins: true,
24 | }),
25 | commonjs(),
26 | json(),
27 | buildAndRun &&
28 | run({
29 | execArgv: ["-r", "source-map-support/register"],
30 | }),
31 | ],
32 | };
33 |
--------------------------------------------------------------------------------
/migration/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "openubl-migration"
3 | version = "0.1.0"
4 | edition = "2021"
5 | publish = false
6 |
7 | [lib]
8 | name = "migration"
9 | path = "src/lib.rs"
10 |
11 | [dependencies]
12 | async-std = { version = "1", features = ["attributes", "tokio1"] }
13 |
14 | [dependencies.sea-orm-migration]
15 | version = "1.0.0"
16 | features = [
17 | # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
18 | # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
19 | # e.g.
20 | "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
21 | "sqlx-sqlite", # `DATABASE_DRIVER` feature
22 | "sqlx-postgres", # `DATABASE_DRIVER` feature
23 | ]
24 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/StateNoResults.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useTranslation } from "react-i18next";
3 |
4 | import {
5 | EmptyState,
6 | EmptyStateBody,
7 | EmptyStateIcon,
8 | EmptyStateVariant,
9 | Title,
10 | } from "@patternfly/react-core";
11 | import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon";
12 |
13 | export const StateNoResults: React.FC = () => {
14 | const { t } = useTranslation();
15 |
16 | return (
17 |
18 |
19 |
20 | {t("message.noResultsFoundTitle")}
21 |
22 | {t("message.noResultsFoundBody")}
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/ui/client/src/app/layout/default-layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Page } from "@patternfly/react-core";
4 |
5 | import { HeaderApp } from "./header";
6 | import { SidebarApp } from "./sidebar";
7 | import { Notifications } from "@app/components/Notifications";
8 | import { PageContentWithDrawerProvider } from "@app/components/PageDrawerContext";
9 |
10 | interface DefaultLayoutProps {
11 | children?: React.ReactNode;
12 | }
13 |
14 | export const DefaultLayout: React.FC = ({ children }) => {
15 | return (
16 | } sidebar={} isManagedSidebar>
17 |
18 | {children}
19 |
20 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/ui/client/src/app/oidc.ts:
--------------------------------------------------------------------------------
1 | import { OidcClientSettings, User } from "oidc-client-ts";
2 | import { ENV } from "./env";
3 |
4 | const OIDC_SERVER_URL =
5 | ENV.OIDC_SERVER_URL || "http://localhost:9001/realms/openubl";
6 | const OIDC_CLIENT_ID = ENV.OIDC_CLIENT_ID || "openubl-ui";
7 |
8 | export const oidcClientSettings: OidcClientSettings = {
9 | authority: OIDC_SERVER_URL,
10 | client_id: OIDC_CLIENT_ID,
11 | redirect_uri: window.location.href,
12 | post_logout_redirect_uri: window.location.href.split("?")[0],
13 | };
14 |
15 | export function getUser() {
16 | const oidcStorage = sessionStorage.getItem(
17 | `oidc.user:${OIDC_SERVER_URL}:${OIDC_CLIENT_ID}`
18 | );
19 | if (!oidcStorage) {
20 | return null;
21 | }
22 |
23 | return User.fromStorageString(oidcStorage);
24 | }
25 |
--------------------------------------------------------------------------------
/storage/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "openubl-storage"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | clap = { workspace = true, features = ["derive", "env"] }
10 | minio = { workspace = true }
11 | serde = { workspace = true, features = ["derive"] }
12 | anyhow = { workspace = true }
13 | uuid = { workspace = true, features = ["v4"] }
14 | thiserror = { workspace = true }
15 | zip = { workspace = true }
16 | tempfile = { workspace = true }
17 | reqwest = { workspace = true }
18 | aws-sdk-s3 = { workspace = true }
19 | tokio = { workspace = true }
20 | aws-config = { workspace = true }
21 | aws-smithy-runtime = { workspace = true }
22 | aws-smithy-runtime-api = { workspace = true }
23 |
--------------------------------------------------------------------------------
/ui/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openubl-ui/server",
3 | "version": "0.1.0",
4 | "license": "Apache-2.0",
5 | "private": true,
6 | "type": "module",
7 | "scripts": {
8 | "clean": "rimraf ./dist",
9 | "lint": "eslint .",
10 | "build": "NODE_ENV=production rollup -c",
11 | "start:dev": "NODE_ENV=development ROLLUP_RUN=true rollup -c -w",
12 | "start": "npm run build && node --enable-source-maps dist/index.js"
13 | },
14 | "lint-staged": {
15 | "*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}": "eslint --fix",
16 | "*.{css,json,md,yaml,yml}": "prettier --write"
17 | },
18 | "dependencies": {
19 | "cookie-parser": "^1.4.6",
20 | "ejs": "^3.1.7",
21 | "express": "^4.17.3",
22 | "http-proxy-middleware": "^2.0.6",
23 | "http-terminator": "^3.2.0"
24 | },
25 | "devDependencies": {}
26 | }
27 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/NoDataEmptyState.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | EmptyState,
4 | EmptyStateBody,
5 | EmptyStateIcon,
6 | EmptyStateVariant,
7 | Title,
8 | } from "@patternfly/react-core";
9 | import CubesIcon from "@patternfly/react-icons/dist/esm/icons/cubes-icon";
10 |
11 | export interface NoDataEmptyStateProps {
12 | title: string;
13 | description?: string;
14 | }
15 |
16 | export const NoDataEmptyState: React.FC = ({
17 | title,
18 | description,
19 | }) => {
20 | return (
21 |
22 |
23 |
24 | {title}
25 |
26 | {description && {description}}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/KeyDisplayToggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Button } from "@patternfly/react-core";
3 | import EyeSlashIcon from "@patternfly/react-icons/dist/js/icons/eye-slash-icon";
4 | import EyeIcon from "@patternfly/react-icons/dist/js/icons/eye-icon";
5 |
6 | interface IKeyDisplayToggleProps {
7 | keyName: string;
8 | isKeyHidden: boolean;
9 | onClick: (event: React.MouseEvent) => void;
10 | }
11 |
12 | export const KeyDisplayToggle: React.FC = ({
13 | keyName,
14 | isKeyHidden,
15 | onClick,
16 | }: IKeyDisplayToggleProps) => (
17 |
22 | );
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.versionsBackup
2 |
3 | # Intellij
4 | ###################
5 | .idea
6 | *.iml
7 |
8 | # Eclipse #
9 | ###########
10 | .project
11 | .settings
12 | .classpath
13 |
14 | # NetBeans #
15 | ############
16 | nbactions.xml
17 | nb-configuration.xml
18 | catalog.xml
19 | nbproject
20 |
21 | # Compiled source #
22 | ###################
23 | *.com
24 | *.class
25 | *.dll
26 | *.exe
27 | *.o
28 | *.so
29 |
30 | # Packages #
31 | ############
32 | # it's better to unpack these files and commit the raw source
33 | # git has its own built in compression methods
34 | *.7z
35 | *.dmg
36 | *.gz
37 | *.iso
38 | *.jar
39 | *.rar
40 | *.tar
41 | *.zip
42 |
43 | # Logs and databases #
44 | ######################
45 | *.log
46 |
47 | # Maven #
48 | #########
49 | target
50 |
51 | # Maven shade
52 | #############
53 | *dependency-reduced-pom.xml
54 |
55 | /lsp/
56 |
57 |
58 | # Added by cargo
59 |
60 | /target
61 |
62 | .openubl/
--------------------------------------------------------------------------------
/ui/client/src/app/queries/ubl-documents.ts:
--------------------------------------------------------------------------------
1 | import { keepPreviousData, useQuery } from "@tanstack/react-query";
2 |
3 | import { HubRequestParams } from "@app/api/models";
4 | import { getUblDocuments } from "@app/api/rest";
5 |
6 | export const UblDocumentsQueryKey = "documents";
7 |
8 | export const useFetchUblDocuments = (
9 | projectId?: number | string,
10 | params: HubRequestParams = {}
11 | ) => {
12 | const { data, isLoading, error, refetch } = useQuery({
13 | enabled: projectId !== undefined,
14 | queryKey: [UblDocumentsQueryKey, projectId, params],
15 | queryFn: () => getUblDocuments(projectId, params),
16 | placeholderData: keepPreviousData,
17 | });
18 | return {
19 | result: {
20 | data: data?.data || [],
21 | total: data?.total ?? 0,
22 | params: data?.params ?? params,
23 | },
24 | isFetching: isLoading,
25 | fetchError: error,
26 | refetch,
27 | };
28 | };
29 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/StateError.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | EmptyState,
4 | EmptyStateIcon,
5 | EmptyStateVariant,
6 | Title,
7 | EmptyStateBody,
8 | } from "@patternfly/react-core";
9 | import ExclamationCircleIcon from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon";
10 | import { global_danger_color_200 as globalDangerColor200 } from "@patternfly/react-tokens";
11 |
12 | export const StateError: React.FC = () => {
13 | return (
14 |
15 |
19 |
20 | Unable to connect
21 |
22 |
23 | There was an error retrieving data. Check your connection and try again.
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/entity/src/send_rule.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
2 |
3 | use sea_orm::entity::prelude::*;
4 |
5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
6 | #[sea_orm(table_name = "send_rule")]
7 | pub struct Model {
8 | #[sea_orm(primary_key)]
9 | pub id: i32,
10 | pub supplier_id: String,
11 | pub credentials_id: i32,
12 | }
13 |
14 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
15 | pub enum Relation {
16 | #[sea_orm(
17 | belongs_to = "super::credentials::Entity",
18 | from = "Column::CredentialsId",
19 | to = "super::credentials::Column::Id",
20 | on_update = "NoAction",
21 | on_delete = "Cascade"
22 | )]
23 | Credentials,
24 | }
25 |
26 | impl Related for Entity {
27 | fn to() -> RelationDef {
28 | Relation::Credentials.def()
29 | }
30 | }
31 |
32 | impl ActiveModelBehavior for ActiveModel {}
33 |
--------------------------------------------------------------------------------
/entity/src/keystore_config.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
2 |
3 | use sea_orm::entity::prelude::*;
4 |
5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
6 | #[sea_orm(table_name = "keystore_config")]
7 | pub struct Model {
8 | #[sea_orm(primary_key)]
9 | pub id: i32,
10 | pub name: String,
11 | pub val: String,
12 | pub keystore_id: i32,
13 | }
14 |
15 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
16 | pub enum Relation {
17 | #[sea_orm(
18 | belongs_to = "super::keystore::Entity",
19 | from = "Column::KeystoreId",
20 | to = "super::keystore::Column::Id",
21 | on_update = "NoAction",
22 | on_delete = "Cascade"
23 | )]
24 | Keystore,
25 | }
26 |
27 | impl Related for Entity {
28 | fn to() -> RelationDef {
29 | Relation::Keystore.def()
30 | }
31 | }
32 |
33 | impl ActiveModelBehavior for ActiveModel {}
34 |
--------------------------------------------------------------------------------
/migration/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub use sea_orm_migration::prelude::*;
2 |
3 | mod m20240101_104121_create_document;
4 | mod m20240113_213636_create_keystore;
5 | mod m20240113_213657_create_keystore_config;
6 | mod m20240114_154538_create_credentials;
7 | mod m20240117_142858_create_send_rule;
8 | mod m20240717_214515_create_delivery;
9 | pub struct Migrator;
10 |
11 | #[async_trait::async_trait]
12 | impl MigratorTrait for Migrator {
13 | fn migrations() -> Vec> {
14 | vec![
15 | Box::new(m20240101_104121_create_document::Migration),
16 | Box::new(m20240113_213636_create_keystore::Migration),
17 | Box::new(m20240113_213657_create_keystore_config::Migration),
18 | Box::new(m20240114_154538_create_credentials::Migration),
19 | Box::new(m20240117_142858_create_send_rule::Migration),
20 | Box::new(m20240717_214515_create_delivery::Migration),
21 | ]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ublhub",
3 | "build": {
4 | "dockerfile": "Dockerfile"
5 | },
6 | "features": {
7 | "ghcr.io/devcontainers/features/rust:1": {},
8 | "ghcr.io/devcontainers/features/node:1": {
9 | "version": "20"
10 | },
11 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}
12 | },
13 | "postCreateCommand": "bash .devcontainer/postCreateCommand.sh",
14 | "customizations": {
15 | "vscode": {
16 | "settings": {},
17 | "extensions": [
18 | "vadimcn.vscode-lldb",
19 | "rust-lang.rust-analyzer",
20 | "tamasfe.even-better-toml",
21 | "github.vscode-github-actions",
22 | "github.vscode-pull-request-github"
23 | ]
24 | },
25 | "codespaces": {
26 | "openFiles": [
27 | "README.md"
28 | ]
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/entity/src/credentials.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
2 |
3 | use sea_orm::entity::prelude::*;
4 |
5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
6 | #[sea_orm(table_name = "credentials")]
7 | pub struct Model {
8 | #[sea_orm(primary_key)]
9 | pub id: i32,
10 | pub name: String,
11 | pub description: Option,
12 | pub username_sol: String,
13 | pub password_sol: String,
14 | pub client_id: String,
15 | pub client_secret: String,
16 | pub url_invoice: String,
17 | pub url_despatch: String,
18 | pub url_perception_retention: String,
19 | }
20 |
21 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
22 | pub enum Relation {
23 | #[sea_orm(has_many = "super::send_rule::Entity")]
24 | SendRule,
25 | }
26 |
27 | impl Related for Entity {
28 | fn to() -> RelationDef {
29 | Relation::SendRule.def()
30 | }
31 | }
32 |
33 | impl ActiveModelBehavior for ActiveModel {}
34 |
--------------------------------------------------------------------------------
/ui/client/src/mocks/browser.ts:
--------------------------------------------------------------------------------
1 | import { type RestHandler, setupWorker, rest } from "msw";
2 |
3 | import config from "./config";
4 | import stubNewWork from "./stub-new-work";
5 |
6 | /**
7 | * Handler to catch unhandled traffic to `/hub/*`, log it, and pass it through to the
8 | * server to handle. This is useful to see traffic, in the console logs, that is not
9 | * being mocked elsewhere.
10 | */
11 | const passthroughHandler: RestHandler = rest.all("/hub/*", (req) => {
12 | console.log(
13 | "%cmsw passthrough%c \u{1fa83} %s",
14 | "font-weight: bold",
15 | "font-weight: normal",
16 | req.url
17 | );
18 | return req.passthrough();
19 | });
20 |
21 | const handlers = [
22 | // TODO: Add handlers for a FULL hub mock data set
23 | ...stubNewWork,
24 | config.passthrough && passthroughHandler,
25 | ].filter(Boolean);
26 |
27 | /**
28 | * A setup MSW browser service worker using the handlers configured in the MOCK env var.
29 | */
30 | export const worker = setupWorker(...handlers);
31 |
32 | export { config } from "./config";
33 |
--------------------------------------------------------------------------------
/ui/common/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openubl-ui/common",
3 | "description": "ESM module for code common to client and server",
4 | "version": "0.1.0",
5 | "license": "Apache-2.0",
6 | "private": true,
7 | "type": "module",
8 | "exports": {
9 | ".": {
10 | "types": "./dist/index.d.ts",
11 | "import": "./dist/index.mjs",
12 | "require": "./dist/index.cjs"
13 | }
14 | },
15 | "types": "./dist",
16 | "main": "./dist/index.cjs",
17 | "module": "./dist/index.mjs",
18 | "scripts": {
19 | "clean": "rimraf ./dist",
20 | "lint": "eslint .",
21 | "prebuild": "npm run clean",
22 | "build": "NODE_ENV=production rollup -c",
23 | "start:dev": "NODE_ENV=development rollup -c --watch"
24 | },
25 | "lint-staged": {
26 | "*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}": "eslint --fix",
27 | "*.{css,json,md,yaml,yml}": "prettier --write"
28 | },
29 | "dependencies": {
30 | "ejs": "^3.1.7",
31 | "express": "^4.17.3",
32 | "http-proxy-middleware": "^2.0.6"
33 | },
34 | "devDependencies": {}
35 | }
36 |
--------------------------------------------------------------------------------
/ui/client/public/index.html.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <% if(brandType == 'lf'){ %>
5 | LibreFact
6 |
7 |
8 | <% } else{ %>
9 | Openubl
10 |
11 |
12 | <% } %>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/ui/client/src/app/context/ProjectContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState } from "react";
2 |
3 | interface IProjectContext {
4 | projectId?: number | string;
5 | setProjectId: (projectId: number | string) => void;
6 | }
7 |
8 | const ProjectContext = createContext({
9 | projectId: undefined,
10 | setProjectId: () => undefined,
11 | });
12 |
13 | interface IProjectContextProviderProps {
14 | children: React.ReactNode;
15 | }
16 |
17 | export const ProjectContextProvider: React.FC = ({
18 | children,
19 | }: IProjectContextProviderProps) => {
20 | const [projectid, setProjectId] = useState();
21 |
22 | return (
23 | setProjectId(key),
27 | }}
28 | >
29 | {children}
30 |
31 | );
32 | };
33 |
34 | export const useProjectContext = (): IProjectContext =>
35 | useContext(ProjectContext);
36 |
--------------------------------------------------------------------------------
/deploy/compose/scripts/keycloak/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -l
2 | KEYCLOAK_HOME="/opt/keycloak"
3 |
4 | while ! $KEYCLOAK_HOME/bin/kcadm.sh config credentials config --server "$KEYCLOAK_SERVER_URL" --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD" &> /dev/null; do
5 | echo "Waiting for Keycloak to start up..."
6 | sleep 3
7 | done
8 |
9 | # Create realm
10 | $KEYCLOAK_HOME/bin/kcadm.sh create realms -s realm=openubl -s enabled=true -o
11 |
12 | # Create clients
13 | $KEYCLOAK_HOME/bin/kcadm.sh create clients -r openubl -f - << EOF
14 | {
15 | "clientId": "openubl-api",
16 | "secret": "secret"
17 | }
18 | EOF
19 |
20 | $KEYCLOAK_HOME/bin/kcadm.sh create clients -r openubl -f - << EOF
21 | {
22 | "clientId": "openubl-ui",
23 | "publicClient": true,
24 | "redirectUris": ["*"],
25 | "webOrigins": ["*"]
26 | }
27 | EOF
28 |
29 | # Create user
30 | $KEYCLOAK_HOME/bin/kcadm.sh create users -r=openubl -s username=carlos -s enabled=true
31 | $KEYCLOAK_HOME/bin/kcadm.sh set-password -r=openubl --username carlos --new-password carlos
--------------------------------------------------------------------------------
/ui/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 |
4 | "include": ["src/**/*", "config/**/*", "types/**/*"],
5 |
6 | "compilerOptions": {
7 | "outDir": "dist",
8 | "baseUrl": ".",
9 | "paths": {
10 | "@app/*": ["src/app/*"],
11 | "@assets/*": [
12 | "../node_modules/@patternfly/react-core/dist/styles/assets/*"
13 | ]
14 | },
15 | "typeRoots": ["types/", "node_modules/@types", "../node_modules/@types"],
16 |
17 | "target": "es2020",
18 | "module": "es2020",
19 | "moduleResolution": "bundler",
20 |
21 | "allowJs": true,
22 | "checkJs": false,
23 | "esModuleInterop": true,
24 | "forceConsistentCasingInFileNames": true,
25 | "jsx": "react",
26 | "resolveJsonModule": true,
27 | "skipLibCheck": true,
28 | "sourceMap": true,
29 | "strict": true,
30 |
31 | "noEmit": true
32 | },
33 |
34 | "ts-node": {
35 | "compilerOptions": {
36 | "module": "commonjs" // node16 == commonjs or es2020 depending on package.json/type
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/ui/client/src/app/layout/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavLink } from "react-router-dom";
3 |
4 | import { Nav, NavList, PageSidebar } from "@patternfly/react-core";
5 | import { css } from "@patternfly/react-styles";
6 |
7 | import { LayoutTheme } from "./layout-constants";
8 |
9 | const LINK_CLASS = "pf-v5-c-nav__link";
10 | const ACTIVE_LINK_CLASS = "pf-m-current";
11 |
12 | export const SidebarApp: React.FC = () => {
13 | const renderPageNav = () => {
14 | return (
15 |
29 | );
30 | };
31 |
32 | return {renderPageNav()};
33 | };
34 |
--------------------------------------------------------------------------------
/server/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "openubl-server"
3 | version = "0.1.0"
4 | edition = "2021"
5 | license = "Apache-2.0"
6 | description = "Web service for managing UBL files from SUNAT"
7 |
8 | [dependencies]
9 | openubl-api = { workspace = true }
10 | openubl-common = { workspace = true }
11 | openubl-entity = { workspace = true }
12 | openubl-storage = { workspace = true }
13 |
14 | xhandler = { workspace = true }
15 |
16 | actix-web = { workspace = true }
17 | serde = { workspace = true, features = ["derive"] }
18 | sea-orm = { workspace = true, features = [
19 | "sqlx-sqlite",
20 | "sqlx-postgres",
21 | "runtime-tokio-rustls",
22 | "macros",
23 | ] }
24 | clap = { workspace = true, features = ["derive", "env"] }
25 | anyhow = { workspace = true }
26 | env_logger = { workspace = true }
27 | thiserror = { workspace = true }
28 | utoipa = { workspace = true, features = ["actix_extras"] }
29 | utoipa-swagger-ui = { workspace = true, features = ["actix-web"] }
30 | actix-web-httpauth = { workspace = true }
31 | actix-4-jwt-auth = { workspace = true }
32 | actix-multipart = { workspace = true }
33 | minio = { workspace = true }
34 |
--------------------------------------------------------------------------------
/ui/client/types/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.avif" {
2 | const src: string;
3 | export default src;
4 | }
5 |
6 | declare module "*.bmp" {
7 | const src: string;
8 | export default src;
9 | }
10 |
11 | declare module "*.gif" {
12 | const src: string;
13 | export default src;
14 | }
15 |
16 | declare module "*.jpg" {
17 | const src: string;
18 | export default src;
19 | }
20 |
21 | declare module "*.jpeg" {
22 | const src: string;
23 | export default src;
24 | }
25 |
26 | declare module "*.png" {
27 | const src: string;
28 | export default src;
29 | }
30 |
31 | declare module "*.webp" {
32 | const src: string;
33 | export default src;
34 | }
35 |
36 | declare module "*.svg" {
37 | import * as React from "react";
38 |
39 | export const ReactComponent: React.FC<
40 | React.SVGProps & { title?: string }
41 | >;
42 |
43 | const src: string;
44 | export default src;
45 | }
46 |
47 | declare module "*.css" {
48 | const classes: { readonly [key: string]: string };
49 | export default classes;
50 | }
51 |
52 | declare module "*.xsd" {
53 | const src: string;
54 | export default src;
55 | }
56 | declare module "*.yaml";
57 |
--------------------------------------------------------------------------------
/ui/client/src/app/images/avatar.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/Notifications.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Alert,
4 | AlertActionCloseButton,
5 | AlertGroup,
6 | } from "@patternfly/react-core";
7 | import { NotificationsContext } from "./NotificationsContext";
8 |
9 | export const Notifications: React.FunctionComponent = () => {
10 | const appContext = React.useContext(NotificationsContext);
11 | return (
12 |
13 | {appContext.notifications.map((notification) => {
14 | return (
15 | {
23 | appContext.dismissNotification(notification.title);
24 | }}
25 | />
26 | ),
27 | })}
28 | timeout={notification.timeout ? notification.timeout : 4000}
29 | >
30 | {notification.message}
31 |
32 | );
33 | })}
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/ui/common/src/proxies.ts:
--------------------------------------------------------------------------------
1 | import type { Options } from "http-proxy-middleware";
2 | import { OPENUBL_ENV } from "./environment.js";
3 |
4 | export const proxyMap: Record = {
5 | "/hub": {
6 | target: OPENUBL_ENV.OPENUBL_HUB_URL || "http://localhost:8080",
7 | logLevel: process.env.DEBUG ? "debug" : "info",
8 |
9 | changeOrigin: true,
10 | pathRewrite: {
11 | "^/hub": "",
12 | },
13 |
14 | onProxyReq: (proxyReq, req, _res) => {
15 | // Add the Bearer token to the request if it is not already present, AND if
16 | // the token is part of the request as a cookie
17 | if (req.cookies?.keycloak_cookie && !req.headers["authorization"]) {
18 | proxyReq.setHeader(
19 | "Authorization",
20 | `Bearer ${req.cookies.keycloak_cookie}`
21 | );
22 | }
23 | },
24 | onProxyRes: (proxyRes, req, res) => {
25 | const includesJsonHeaders =
26 | req.headers.accept?.includes("application/json");
27 | if (
28 | (!includesJsonHeaders && proxyRes.statusCode === 401) ||
29 | (!includesJsonHeaders && proxyRes.statusMessage === "Unauthorized")
30 | ) {
31 | res.redirect("/");
32 | }
33 | },
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/migration/src/m20240113_213636_create_keystore.rs:
--------------------------------------------------------------------------------
1 | use sea_orm_migration::prelude::*;
2 |
3 | #[derive(DeriveMigrationName)]
4 | pub struct Migration;
5 |
6 | #[async_trait::async_trait]
7 | impl MigrationTrait for Migration {
8 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9 | manager
10 | .create_table(
11 | Table::create()
12 | .table(Keystore::Table)
13 | .if_not_exists()
14 | .col(
15 | ColumnDef::new(Keystore::Id)
16 | .integer()
17 | .not_null()
18 | .auto_increment()
19 | .primary_key(),
20 | )
21 | .col(ColumnDef::new(Keystore::Name).string().not_null())
22 | .to_owned(),
23 | )
24 | .await
25 | }
26 |
27 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
28 | manager
29 | .drop_table(Table::drop().table(Keystore::Table).to_owned())
30 | .await
31 | }
32 | }
33 |
34 | #[derive(DeriveIden)]
35 | pub enum Keystore {
36 | Table,
37 | Id,
38 | Name,
39 | }
40 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 | members = [
4 | "migration",
5 | "entity",
6 | "common",
7 | "api",
8 | "server",
9 | "cli",
10 | "storage",
11 | "signature",
12 | ]
13 |
14 | default-members = ["cli"]
15 |
16 | [workspace.dependencies]
17 | openubl-entity = { path = "./entity" }
18 | openubl-common = { path = "./common" }
19 | openubl-migration = { path = "./migration" }
20 | openubl-storage = { path = "./storage" }
21 | openubl-server = { path = "./server" }
22 | openubl-api = { path = "./api" }
23 |
24 | xhandler = { git = "https://github.com/project-openubl/xhandler-rust.git", branch = "main"}
25 |
26 | sea-orm = "1.0.0"
27 | sea-query = "0.31"
28 | async-trait = "0.1.75"
29 | anyhow = "1.0.76"
30 | thiserror = "1.0.50"
31 | serde_json = "1.x"
32 | serde = "1.x"
33 | clap = "4.4.11"
34 | actix-web = "4.4.1"
35 | log = "0.4.21"
36 | tokio = "1.38.0"
37 | env_logger = "0.11.5"
38 | utoipa = "4.1.0"
39 | utoipa-swagger-ui = "7.1.0"
40 | actix-web-httpauth = "0.8.1"
41 | actix-4-jwt-auth = "1.2.0"
42 | actix-multipart = "0.7.2"
43 | minio = "0.1.0"
44 | uuid = "1.6.1"
45 | zip = "2.1.6"
46 | tempfile = "3.9.0"
47 | reqwest = "0.11"
48 | aws-sdk-s3 = "1.11.0"
49 | aws-config = "1.1.1"
50 | aws-smithy-runtime = "1.1.1"
51 | aws-smithy-runtime-api = "1.1.1"
52 |
--------------------------------------------------------------------------------
/ui/client/types/@hookform_resolvers_2.9.11.d.ts:
--------------------------------------------------------------------------------
1 | // See: https://github.com/react-hook-form/resolvers/pull/527
2 |
3 | // Upgrading to @hookform/resolves@^3 will properly fix the problem, but until
4 | // that is an option (it is built for yup@1), manually pull the types in.
5 |
6 | declare module "@hookform/resolvers/yup" {
7 | // contents of @hookform/resolvers/yup/dist/types.d.ts
8 | import {
9 | FieldValues,
10 | ResolverOptions,
11 | ResolverResult,
12 | } from "react-hook-form";
13 | import * as Yup from "yup";
14 | import type Lazy from "yup/lib/Lazy";
15 |
16 | declare type Options> = Parameters<
17 | T["validate"]
18 | >[1];
19 | export declare type Resolver = >(
20 | schema: T,
21 | schemaOptions?: Options,
22 | factoryOptions?: {
23 | mode?: "async" | "sync";
24 | rawValues?: boolean;
25 | }
26 | ) => (
27 | values: TFieldValues,
28 | context: TContext | undefined,
29 | options: ResolverOptions
30 | ) => Promise>;
31 | export {};
32 |
33 | // contents of @hookform/resolvers/yup/dist/yup.d.ts
34 | export declare const yupResolver: Resolver;
35 | }
36 |
--------------------------------------------------------------------------------
/ui/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | if [[ -z "$OPENUBL_API_URL" ]]; then
6 | echo "You must provide OPENUBL_API_URL environment variable" 1>&2
7 | exit 1
8 | fi
9 |
10 | if [[ $AUTH_REQUIRED != "false" ]]; then
11 | if [[ -z "$OIDC_CLIENT_ID" ]]; then
12 | echo "You must provide OIDC_CLIENT_ID environment variable" 1>&2
13 | exit 1
14 | fi
15 | if [[ -z "$OIDC_SERVER_URL" ]]; then
16 | echo "You must provide OIDC_SERVER_URL environment variable" 1>&2
17 | exit 1
18 | fi
19 | fi
20 |
21 | # Copy the Kube API and service CA bundle to /opt/app-root/src/ca.crt if they exist
22 |
23 | # Add Kube API CA
24 | if [ -f "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" ]; then
25 | cp /var/run/secrets/kubernetes.io/serviceaccount/ca.crt ${NODE_EXTRA_CA_CERTS}
26 | fi
27 |
28 | # Add service serving CA
29 | if [ -f "/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt" ]; then
30 | cat /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt >> ${NODE_EXTRA_CA_CERTS}
31 | fi
32 |
33 | # Add custom ingress CA if it exists
34 | if [ -f "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" ]; then
35 | cat /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem >> ${NODE_EXTRA_CA_CERTS}
36 | fi
37 |
38 | exec node --enable-source-maps server/dist/index.js
39 |
--------------------------------------------------------------------------------
/ui/client/types/array-filter-Boolean.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Fixes https://github.com/microsoft/TypeScript/issues/16655 for `Array.prototype.filter()`
3 | * For example, using the fix the type of `bar` is `string[]` in the below snippet as it should be.
4 | *
5 | * const foo: (string | null | undefined)[] = [];
6 | * const bar = foo.filter(Boolean);
7 | *
8 | * For related definitions, see https://github.com/microsoft/TypeScript/blob/master/src/lib/es5.d.ts
9 | *
10 | * Original licenses apply, see
11 | * - https://github.com/microsoft/TypeScript/blob/master/LICENSE.txt
12 | * - https://stackoverflow.com/help/licensing
13 | */
14 |
15 | /** See https://stackoverflow.com/a/51390763/1470607 */
16 | type Falsy = false | 0 | "" | null | undefined;
17 |
18 | interface Array {
19 | /**
20 | * Returns the elements of an array that meet the condition specified in a callback function.
21 | * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
22 | * @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
23 | */
24 | filter(
25 | predicate: BooleanConstructor,
26 | thisArg?: any
27 | ): Exclude[];
28 | }
29 |
--------------------------------------------------------------------------------
/ui/client/src/app/axios-config/apiInit.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { getUser } from "@app/oidc";
3 |
4 | export const initInterceptors = () => {
5 | axios.interceptors.request.use(
6 | (config) => {
7 | const user = getUser();
8 | const token = user?.access_token;
9 | if (token) {
10 | config.headers.Authorization = `Bearer ${token}`;
11 | }
12 | return config;
13 | },
14 | (error) => {
15 | return Promise.reject(error);
16 | }
17 | );
18 |
19 | // axios.interceptors.response.use(
20 | // (response) => {
21 | // return response;
22 | // },
23 | // async (error) => {
24 | // if (error.response && error.response.status === 401) {
25 | // try {
26 | // const refreshed = await keycloak.updateToken(5);
27 | // if (refreshed) {
28 | // const retryConfig = {
29 | // ...error.config,
30 | // headers: {
31 | // ...error.config.headers,
32 | // Authorization: `Bearer ${keycloak.token}`,
33 | // },
34 | // };
35 | // return axios(retryConfig);
36 | // }
37 | // } catch (refreshError) {
38 | // keycloak.login();
39 | // }
40 | // }
41 | // return Promise.reject(error);
42 | // }
43 | // );
44 | };
45 |
--------------------------------------------------------------------------------
/ui/client/src/app/Routes.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, lazy } from "react";
2 | import { useParams, useRoutes } from "react-router-dom";
3 |
4 | import { Bullseye, Spinner } from "@patternfly/react-core";
5 |
6 | const Home = lazy(() => import("./pages/home"));
7 | const ProjectList = lazy(() => import("./pages/project-list"));
8 | const ProjectDetails = lazy(() => import("./pages/project-details"));
9 |
10 | export const ViewRepositoryRouteParam = "repositoryId";
11 | export const ViewPackageRouteParam = "packageId";
12 |
13 | export enum PathParam {
14 | PROJECT_ID = "projectId",
15 | }
16 |
17 | export const AppRoutes = () => {
18 | const allRoutes = useRoutes([
19 | { path: "/", element: },
20 | { path: "/projects", element: },
21 | { path: `/projects/:${PathParam.PROJECT_ID}`, element: },
22 | // { path: "/projects/:projectId/documents", element: },
23 | // { path: "*", element: },
24 | ]);
25 |
26 | return (
27 |
30 |
31 |
32 | }
33 | >
34 | {allRoutes}
35 |
36 | );
37 | };
38 |
39 | export const useRouteParams = (pathParam: PathParam) => {
40 | const params = useParams();
41 | return params[pathParam];
42 | };
43 |
--------------------------------------------------------------------------------
/migration/README.md:
--------------------------------------------------------------------------------
1 | # CLI
2 |
3 | Generate migration and apply it:
4 |
5 | ```shell
6 | sea-orm-cli migrate generate NAME_OF_MIGRATION
7 | sea-orm-cli migrate -u postgres://user:password@localhost/openubl
8 | ```
9 |
10 | Generate entity files of database `openubl` to `entity/src`
11 |
12 | ```shell
13 | sea-orm-cli generate entity -u postgres://user:password@localhost/openubl -o entity/src
14 | ```
15 |
16 | # Running Migrator CLI
17 |
18 | - Generate a new migration file
19 | ```sh
20 | cargo run -- generate MIGRATION_NAME
21 | ```
22 | - Apply all pending migrations
23 | ```sh
24 | cargo run
25 | ```
26 | ```sh
27 | cargo run -- up
28 | ```
29 | - Apply first 10 pending migrations
30 | ```sh
31 | cargo run -- up -n 10
32 | ```
33 | - Rollback last applied migrations
34 | ```sh
35 | cargo run -- down
36 | ```
37 | - Rollback last 10 applied migrations
38 | ```sh
39 | cargo run -- down -n 10
40 | ```
41 | - Drop all tables from the database, then reapply all migrations
42 | ```sh
43 | cargo run -- fresh
44 | ```
45 | - Rollback all applied migrations, then reapply all migrations
46 | ```sh
47 | cargo run -- refresh
48 | ```
49 | - Rollback all applied migrations
50 | ```sh
51 | cargo run -- reset
52 | ```
53 | - Check the status of all migrations
54 | ```sh
55 | cargo run -- status
56 | ```
57 |
--------------------------------------------------------------------------------
/ui/client/public/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": {
3 | "cancel": "Cancel",
4 | "clearAllFilters": "Clear all filters",
5 | "createNew": "Create new",
6 | "delete": "Delete",
7 | "edit": "Edit",
8 | "filterBy": "Filter by {{what}}",
9 | "selectAll": "Select all",
10 | "selectNone": "Select none",
11 | "selectPage": "Select page"
12 | },
13 | "dialog": {
14 | "message": {
15 | "delete": "This action cannot be undone"
16 | },
17 | "title": {
18 | "deleteWithName": "Delete {{what}} \"{{name}}\"",
19 | "new": "New {{what}}"
20 | }
21 | },
22 | "message": {
23 | "noDataAvailableBody": "No data available",
24 | "noDataAvailableTitle": "No data available",
25 | "noResultsFoundBody": "No resulst found",
26 | "noResultsFoundTitle": "No results found"
27 | },
28 | "terms": {
29 | "cancel": "Cancel",
30 | "create": "Create",
31 | "description": "Description",
32 | "name": "Name",
33 | "project": "Project",
34 | "projectDeleted": "Project deleted",
35 | "projects": "Projects",
36 | "save": "Save"
37 | },
38 | "toastr": {
39 | "fail": {
40 | "create": "Failed to create {{type}}.",
41 | "save": "Failed to save {{type}}."
42 | },
43 | "success": {
44 | "created": "{{type}} was successfully created.",
45 | "saved": "{{type}} was successfully saved."
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/ui/Dockerfile:
--------------------------------------------------------------------------------
1 | # Builder image
2 | FROM registry.access.redhat.com/ubi9/nodejs-18:latest as builder
3 |
4 | USER 1001
5 | COPY --chown=1001 . .
6 | RUN npm clean-install && npm run build && npm run dist
7 |
8 | # Runner image
9 | FROM registry.access.redhat.com/ubi9/nodejs-18-minimal:latest
10 |
11 | # Add ps package to allow liveness probe for k8s cluster
12 | # Add tar package to allow copying files with kubectl scp
13 | USER 0
14 | RUN microdnf -y install tar procps-ng && microdnf clean all
15 |
16 | USER 1001
17 |
18 | LABEL name="project-openubl/openubl-ui" \
19 | description="Openubl - User Interface" \
20 | help="For more information visit https://project-openubl.github.io/" \
21 | license="Apache License 2.0" \
22 | maintainer="carlosthe19916@gmail.com" \
23 | summary="Openubl - User Interface" \
24 | url="https://quay.io/repository/projectopenubl/openubl-ui" \
25 | usage="podman run -p 80 -v projectopenubl/openubl-ui:latest" \
26 | io.k8s.display-name="openubl-ui" \
27 | io.k8s.description="Openubl - User Interface" \
28 | io.openshift.expose-services="80:http" \
29 | io.openshift.tags="operator,openubl,ui,nodejs18" \
30 | io.openshift.min-cpu="100m" \
31 | io.openshift.min-memory="350Mi"
32 |
33 | COPY --from=builder /opt/app-root/src/dist /opt/app-root/dist/
34 |
35 | ENV DEBUG=1
36 |
37 | WORKDIR /opt/app-root/dist
38 | ENTRYPOINT ["./entrypoint.sh"]
39 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/AppTableActionButtons.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button, Flex, FlexItem } from "@patternfly/react-core";
3 | import { ConditionalTooltip } from "./ConditionalTooltip";
4 |
5 | export interface AppTableActionButtonsProps {
6 | isDeleteEnabled?: boolean;
7 | tooltipMessage?: string;
8 | onEdit: () => void;
9 | onDelete: () => void;
10 | }
11 |
12 | export const AppTableActionButtons: React.FC = ({
13 | isDeleteEnabled = false,
14 | tooltipMessage = "",
15 | onEdit,
16 | onDelete,
17 | }) => {
18 | return (
19 | <>
20 |
21 |
22 |
30 |
31 |
32 |
36 |
45 |
46 |
47 |
48 | >
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/ui/client/public/locales/es/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": {
3 | "cancel": "Cancelar",
4 | "clearAllFilters": "Limpiar todos los filtros",
5 | "createNew": "Crear nuevo",
6 | "delete": "Eliminar",
7 | "edit": "Editar",
8 | "filterBy": "Filtrar por {{what}}",
9 | "selectAll": "Seleccionar todos",
10 | "selectNone": "Seleccionar ninguno",
11 | "selectPage": "Seleccionar página"
12 | },
13 | "dialog": {
14 | "message": {
15 | "delete": "Esta acción no puede ser revertida."
16 | },
17 | "title": {
18 | "deleteWithName": "Eliminar {{what}} \"{{name}}\"?",
19 | "new": "Nuevo {{what}}"
20 | }
21 | },
22 | "message": {
23 | "noDataAvailableBody": "No hay datos disponibles para ser mostrados aquí.",
24 | "noDataAvailableTitle": "No hay datos disponibles",
25 | "noResultsFoundBody": "Ningún resultado coincide con los criterios del filtro. Elimine todos los filtros o borre todos los filtros para mostrar los resultados.",
26 | "noResultsFoundTitle": "No se han encontrado resultados"
27 | },
28 | "terms": {
29 | "cancel": "Cancelar",
30 | "create": "Crear",
31 | "description": "Descripción",
32 | "name": "Nombre",
33 | "project": "Proyecto",
34 | "projectDeleted": "Proyecto eliminado",
35 | "projects": "Proyectos",
36 | "save": "Guardar"
37 | },
38 | "toastr": {
39 | "fail": {
40 | "create": "Error al crear {{type}}.",
41 | "save": "Error al guardar {{type}}."
42 | },
43 | "success": {
44 | "created": "{{type}} fue creado.",
45 | "saved": "{{type}} fue guardado."
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/OidcProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from "react";
2 | import { AuthProvider, useAuth } from "react-oidc-context";
3 | import { oidcClientSettings } from "@app/oidc";
4 | import ENV from "@app/env";
5 | import { AppPlaceholder } from "./AppPlaceholder";
6 | import { initInterceptors } from "@app/axios-config";
7 |
8 | interface IOidcProviderProps {
9 | children: React.ReactNode;
10 | }
11 |
12 | export const OidcProvider: React.FC = ({ children }) => {
13 | return ENV.AUTH_REQUIRED !== "true" ? (
14 | <>{children}>
15 | ) : (
16 |
20 | window.history.replaceState(
21 | {},
22 | document.title,
23 | window.location.pathname
24 | )
25 | }
26 | >
27 | {children}
28 |
29 | );
30 | };
31 |
32 | const AuthEnabledOidcProvider: React.FC = ({
33 | children,
34 | }) => {
35 | const auth = useAuth();
36 |
37 | React.useEffect(() => {
38 | if (!auth.isAuthenticated && !auth.isLoading) {
39 | auth.signinRedirect();
40 | }
41 | }, [auth.isAuthenticated, auth.isLoading]);
42 |
43 | React.useEffect(() => {
44 | initInterceptors();
45 | }, []);
46 |
47 | if (auth.isAuthenticated) {
48 | return }>{children};
49 | } else if (auth.isLoading) {
50 | return ;
51 | } else {
52 | return Login in...
;
53 | }
54 | };
55 |
--------------------------------------------------------------------------------
/ui/client/src/app/api/models.ts:
--------------------------------------------------------------------------------
1 | export enum MimeType {
2 | TAR = "tar",
3 | YAML = "yaml",
4 | }
5 |
6 | /** Mark an object as "New" therefore does not have an `id` field. */
7 | export type New = Omit;
8 |
9 | export interface HubFilter {
10 | field: string;
11 | operator?: "=" | "!=" | "~" | ">" | ">=" | "<" | "<=";
12 | value:
13 | | string
14 | | number
15 | | {
16 | list: (string | number)[];
17 | operator?: "AND" | "OR";
18 | };
19 | }
20 |
21 | export interface HubRequestParams {
22 | filters?: HubFilter[];
23 | sort?: {
24 | field: string;
25 | direction: "asc" | "desc";
26 | };
27 | page?: {
28 | pageNumber: number; // 1-indexed
29 | itemsPerPage: number;
30 | };
31 | }
32 |
33 | export interface HubPaginatedResult {
34 | data: T[];
35 | total: number;
36 | params: HubRequestParams;
37 | }
38 |
39 | //
40 |
41 | export interface Project {
42 | id: number;
43 | name: string;
44 | description?: string;
45 | }
46 |
47 | export interface UblDocument {
48 | id: number;
49 | file_id: string;
50 | document_type: string;
51 | document_id: string;
52 | supplier_id: string;
53 | voided_document_code?: string;
54 | }
55 |
56 | export interface Credentials {
57 | id: number;
58 | name: string;
59 | description?: string;
60 | supplier_ids_applied_to: string[];
61 | soap?: {
62 | username_sol: string;
63 | password_sol: string;
64 | url_invoice: string;
65 | url_perception_retention: string;
66 | };
67 | rest?: {
68 | client_id: string;
69 | client_secret: string;
70 | url_despatch: string;
71 | };
72 | }
73 |
--------------------------------------------------------------------------------
/storage/src/config.rs:
--------------------------------------------------------------------------------
1 | #[derive(clap::Subcommand, Debug)]
2 | pub enum Storage {
3 | Local(LocalStorage),
4 | Minio(MinioStorage),
5 | S3(S3Storage),
6 | }
7 |
8 | #[derive(clap::Args, Debug)]
9 | pub struct LocalStorage {
10 | #[arg(
11 | id = "storage-local-dir",
12 | long,
13 | env = "STORAGE_LOCAL_DIR",
14 | default_value = "storage"
15 | )]
16 | pub local_dir: String,
17 | }
18 |
19 | #[derive(clap::Args, Debug)]
20 | pub struct MinioStorage {
21 | #[arg(id = "storage-minio-host", long, env = "STORAGE_MINIO_HOST")]
22 | pub host: String,
23 |
24 | #[arg(
25 | id = "storage-minio-bucket",
26 | long,
27 | env = "STORAGE_MINIO_BUCKET",
28 | default_value = "openubl"
29 | )]
30 | pub bucket: String,
31 |
32 | #[arg(
33 | id = "storage-minio-access-key",
34 | long,
35 | env = "STORAGE_MINIO_ACCESS_KEY"
36 | )]
37 | pub access_key: String,
38 |
39 | #[arg(
40 | id = "storage-minio-secret-key",
41 | long,
42 | env = "STORAGE_MINIO_SECRET_KEY"
43 | )]
44 | pub secret_key: String,
45 | }
46 |
47 | #[derive(clap::Args, Debug)]
48 | pub struct S3Storage {
49 | #[arg(id = "storage-s3-region", long, env = "STORAGE_S3_REGION")]
50 | pub region: String,
51 |
52 | #[arg(
53 | id = "storage-s3-bucket",
54 | long,
55 | env = "STORAGE_S3_BUCKET",
56 | default_value = "openubl"
57 | )]
58 | pub bucket: String,
59 |
60 | #[arg(id = "storage-s3-access-key", long, env = "STORAGE_S3_ACCESS_KEY")]
61 | pub access_key: String,
62 |
63 | #[arg(id = "storage-s3-secret-key", long, env = "STORAGE_S3_SECRET_KEY")]
64 | pub secret_key: String,
65 | }
66 |
--------------------------------------------------------------------------------
/migration/src/m20240117_142858_create_send_rule.rs:
--------------------------------------------------------------------------------
1 | use crate::m20240114_154538_create_credentials::Credentials;
2 | use sea_orm_migration::prelude::*;
3 |
4 | #[derive(DeriveMigrationName)]
5 | pub struct Migration;
6 |
7 | #[async_trait::async_trait]
8 | impl MigrationTrait for Migration {
9 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
10 | manager
11 | .create_table(
12 | Table::create()
13 | .table(SendRule::Table)
14 | .if_not_exists()
15 | .col(
16 | ColumnDef::new(SendRule::Id)
17 | .integer()
18 | .not_null()
19 | .auto_increment()
20 | .primary_key(),
21 | )
22 | .col(ColumnDef::new(SendRule::SupplierId).string().not_null())
23 | .col(ColumnDef::new(SendRule::CredentialsId).integer().not_null())
24 | .foreign_key(
25 | ForeignKey::create()
26 | .from_col(SendRule::CredentialsId)
27 | .to(Credentials::Table, Credentials::Id)
28 | .on_delete(ForeignKeyAction::Cascade),
29 | )
30 | .to_owned(),
31 | )
32 | .await
33 | }
34 |
35 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
36 | manager
37 | .drop_table(Table::drop().table(SendRule::Table).to_owned())
38 | .await
39 | }
40 | }
41 |
42 | #[derive(DeriveIden)]
43 | enum SendRule {
44 | Table,
45 | Id,
46 | SupplierId,
47 | CredentialsId,
48 | }
49 |
--------------------------------------------------------------------------------
/ui/client/src/app/pages/project-details/project-details.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | import {
5 | Breadcrumb,
6 | BreadcrumbItem,
7 | PageSection,
8 | PageSectionVariants,
9 | Tab,
10 | TabTitleText,
11 | Tabs,
12 | Text,
13 | TextContent
14 | } from "@patternfly/react-core";
15 |
16 | import {
17 | useFetchProjectById
18 | } from "@app/queries/projects";
19 |
20 | import { PathParam, useRouteParams } from "@app/Routes";
21 | import { CredentialsTable } from "./credentials/credentials-table";
22 |
23 | export const ProjectDetails: React.FC = () => {
24 | const projectId = useRouteParams(PathParam.PROJECT_ID);
25 | const { project } = useFetchProjectById(projectId);
26 |
27 | return (
28 | <>
29 |
30 |
31 |
32 | Proyectos
33 |
34 |
35 | Proyecto detalles
36 |
37 |
38 |
39 |
40 |
41 | {project?.name}
42 |
43 |
44 |
45 | Documentos}>
46 | documentos
47 |
48 | Credenciales}>
49 | {projectId && }
50 |
51 |
52 |
53 | >
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/ui/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 |
4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
6 |
7 | import ENV from "@app/env";
8 | import App from "@app/App";
9 | import reportWebVitals from "@app/reportWebVitals";
10 |
11 | import dayjs from "dayjs";
12 | import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
13 | import utc from "dayjs/plugin/utc";
14 | import timezone from "dayjs/plugin/timezone";
15 | import customParseFormat from "dayjs/plugin/customParseFormat";
16 | import { OidcProvider } from "@app/components/OidcProvider";
17 |
18 | dayjs.extend(utc);
19 | dayjs.extend(timezone);
20 | dayjs.extend(customParseFormat);
21 | dayjs.extend(isSameOrBefore);
22 |
23 | const queryClient = new QueryClient();
24 |
25 | const root = ReactDOM.createRoot(
26 | document.getElementById("root") as HTMLElement
27 | );
28 |
29 | const renderApp = () => {
30 | return root.render(
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | if (ENV.NODE_ENV === "development") {
43 | import("./mocks/browser").then((browserMocks) => {
44 | if (browserMocks.config.enabled) {
45 | browserMocks.worker.start();
46 | }
47 | renderApp();
48 | });
49 | } else {
50 | renderApp();
51 | }
52 |
53 | // If you want to start measuring performance in your app, pass a function
54 | // to log results (for example: reportWebVitals(console.log))
55 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
56 | reportWebVitals();
57 |
--------------------------------------------------------------------------------
/migration/src/m20240101_104121_create_document.rs:
--------------------------------------------------------------------------------
1 | use sea_orm_migration::prelude::*;
2 |
3 | #[derive(DeriveMigrationName)]
4 | pub struct Migration;
5 |
6 | #[async_trait::async_trait]
7 | impl MigrationTrait for Migration {
8 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9 | manager
10 | .create_table(
11 | Table::create()
12 | .table(Document::Table)
13 | .if_not_exists()
14 | .col(
15 | ColumnDef::new(Document::Id)
16 | .integer()
17 | .not_null()
18 | .auto_increment()
19 | .primary_key(),
20 | )
21 | .col(ColumnDef::new(Document::FileId).string().not_null())
22 | .col(ColumnDef::new(Document::SupplierId).string().not_null())
23 | .col(ColumnDef::new(Document::Identifier).string().not_null())
24 | .col(ColumnDef::new(Document::Type).string().not_null())
25 | .col(ColumnDef::new(Document::VoidedDocumentCode).string())
26 | .col(ColumnDef::new(Document::DigestValue).string())
27 | .col(ColumnDef::new(Document::Sha256).string().not_null())
28 | .to_owned(),
29 | )
30 | .await
31 | }
32 |
33 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
34 | manager
35 | .drop_table(Table::drop().table(Document::Table).to_owned())
36 | .await
37 | }
38 | }
39 |
40 | #[derive(DeriveIden)]
41 | pub enum Document {
42 | Table,
43 | Id,
44 | FileId,
45 | Type,
46 | Identifier,
47 | SupplierId,
48 | VoidedDocumentCode,
49 | DigestValue,
50 | Sha256,
51 | }
52 |
--------------------------------------------------------------------------------
/ui/client/src/app/hooks/table-controls/getPaginationHubRequestParams.ts:
--------------------------------------------------------------------------------
1 | import { HubRequestParams } from "@app/api/models";
2 | import { PaginationState } from "@mturley-latest/react-table-batteries";
3 |
4 | /**
5 | * Args for getPaginationHubRequestParams
6 | * - Partially satisfied by the object returned by useTableControlState (ITableControlState)
7 | */
8 | export interface IGetPaginationHubRequestParamsArgs {
9 | /**
10 | * The "source of truth" state for the pagination feature (returned by usePaginationState)
11 | */
12 | pagination?: PaginationState;
13 | }
14 |
15 | /**
16 | * Given the state for the pagination feature and additional arguments, returns params the hub API needs to apply the current pagination.
17 | * - Makes up part of the object returned by getHubRequestParams
18 | * @see getHubRequestParams
19 | */
20 | export const getPaginationHubRequestParams = ({
21 | pagination: paginationState,
22 | }: IGetPaginationHubRequestParamsArgs): Partial => {
23 | if (!paginationState) return {};
24 | const { pageNumber, itemsPerPage } = paginationState;
25 | return { page: { pageNumber, itemsPerPage } };
26 | };
27 |
28 | /**
29 | * Converts the values returned by getPaginationHubRequestParams into the URL query strings expected by the hub API
30 | * - Appends converted URL params to the given `serializedParams` object for use in the hub API request
31 | * - Constructs part of the object returned by serializeRequestParamsForHub
32 | * @see serializeRequestParamsForHub
33 | */
34 | export const serializePaginationRequestParamsForHub = (
35 | deserializedParams: HubRequestParams,
36 | serializedParams: URLSearchParams
37 | ) => {
38 | const { page } = deserializedParams;
39 | if (page) {
40 | const { pageNumber, itemsPerPage } = page;
41 | serializedParams.append("limit", String(itemsPerPage));
42 | serializedParams.append("offset", String((pageNumber - 1) * itemsPerPage));
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/HookFormPFFields/HookFormPFTextArea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { FieldValues, Path } from "react-hook-form";
3 | import { TextArea, TextAreaProps } from "@patternfly/react-core";
4 | import { getValidatedFromErrors } from "@app/utils/utils";
5 | import {
6 | HookFormPFGroupController,
7 | BaseHookFormPFGroupControllerProps,
8 | extractGroupControllerProps,
9 | } from "./HookFormPFGroupController";
10 |
11 | export type HookFormPFTextAreaProps<
12 | TFieldValues extends FieldValues,
13 | TName extends Path,
14 | > = TextAreaProps & BaseHookFormPFGroupControllerProps;
15 |
16 | export const HookFormPFTextArea = <
17 | TFieldValues extends FieldValues = FieldValues,
18 | TName extends Path = Path,
19 | >(
20 | props: HookFormPFTextAreaProps
21 | ) => {
22 | const { extractedProps, remainingProps } = extractGroupControllerProps<
23 | TFieldValues,
24 | TName,
25 | HookFormPFTextAreaProps
26 | >(props);
27 | const { fieldId, helperText, isRequired, errorsSuppressed } = extractedProps;
28 | return (
29 |
30 | {...extractedProps}
31 | renderInput={({
32 | field: { onChange, onBlur, value, name, ref },
33 | fieldState: { isDirty, error, isTouched },
34 | }) => (
35 |
51 | )}
52 | />
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/migration/src/m20240113_213657_create_keystore_config.rs:
--------------------------------------------------------------------------------
1 | use crate::m20240113_213636_create_keystore::Keystore;
2 | use sea_orm_migration::prelude::*;
3 |
4 | #[derive(DeriveMigrationName)]
5 | pub struct Migration;
6 |
7 | #[async_trait::async_trait]
8 | impl MigrationTrait for Migration {
9 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
10 | manager
11 | .create_table(
12 | Table::create()
13 | .table(KeystoreConfig::Table)
14 | .if_not_exists()
15 | .col(
16 | ColumnDef::new(KeystoreConfig::Id)
17 | .integer()
18 | .not_null()
19 | .auto_increment()
20 | .primary_key(),
21 | )
22 | .col(ColumnDef::new(KeystoreConfig::Name).string().not_null())
23 | .col(ColumnDef::new(KeystoreConfig::Val).string().not_null())
24 | .col(
25 | ColumnDef::new(KeystoreConfig::KeystoreId)
26 | .integer()
27 | .not_null(),
28 | )
29 | .foreign_key(
30 | ForeignKey::create()
31 | .from_col(KeystoreConfig::KeystoreId)
32 | .to(Keystore::Table, Keystore::Id)
33 | .on_delete(ForeignKeyAction::Cascade),
34 | )
35 | .to_owned(),
36 | )
37 | .await
38 | }
39 |
40 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
41 | manager
42 | .drop_table(Table::drop().table(KeystoreConfig::Table).to_owned())
43 | .await
44 | }
45 | }
46 |
47 | #[derive(DeriveIden)]
48 | enum KeystoreConfig {
49 | Table,
50 | Id,
51 | Name,
52 | Val,
53 | KeystoreId,
54 | }
55 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/NotificationsContext.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { AlertProps } from "@patternfly/react-core";
3 |
4 | export type INotification = {
5 | title: string;
6 | variant: AlertProps["variant"];
7 | message?: React.ReactNode;
8 | hideCloseButton?: boolean;
9 | timeout?: number | boolean;
10 | };
11 |
12 | interface INotificationsProvider {
13 | children: React.ReactNode;
14 | }
15 |
16 | interface INotificationsContext {
17 | pushNotification: (notification: INotification) => void;
18 | dismissNotification: (key: string) => void;
19 | notifications: INotification[];
20 | }
21 |
22 | const appContextDefaultValue = {} as INotificationsContext;
23 |
24 | const notificationDefault: Pick = {
25 | hideCloseButton: false,
26 | };
27 |
28 | export const NotificationsContext = React.createContext(
29 | appContextDefaultValue
30 | );
31 |
32 | export const NotificationsProvider: React.FunctionComponent<
33 | INotificationsProvider
34 | > = ({ children }: INotificationsProvider) => {
35 | const [notifications, setNotifications] = React.useState([]);
36 |
37 | const pushNotification = (
38 | notification: INotification,
39 | clearNotificationDelay?: number
40 | ) => {
41 | setNotifications([
42 | ...notifications,
43 | { ...notificationDefault, ...notification },
44 | ]);
45 | setTimeout(() => setNotifications([]), clearNotificationDelay || 10000);
46 | };
47 |
48 | const dismissNotification = (title: string) => {
49 | const remainingNotifications = notifications.filter(
50 | (n) => n.title !== title
51 | );
52 | setNotifications(remainingNotifications);
53 | };
54 |
55 | return (
56 |
63 | {children}
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/ui/scripts/verify_lock.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import process from "node:process";
4 | import path from "node:path";
5 | import { readFileSync } from "node:fs";
6 |
7 | // set the working directory to project root
8 | // fs.accessSync("./package-lock.json")
9 | const getProjectRoot = () => path.resolve(path.dirname(process.argv[1]), "../");
10 | process.chdir(getProjectRoot());
11 |
12 | // load the lock file
13 | const lockFilePath = path.resolve(process.cwd(), "package-lock.json");
14 | const lockFile = JSON.parse(readFileSync(lockFilePath));
15 |
16 | const toLog = {
17 | name: lockFile.name,
18 | version: lockFile.version,
19 | lockfileVersion: lockFile.lockfileVersion,
20 | };
21 |
22 | // check the packages packages
23 | const removeUndefined = (obj) =>
24 | Object.fromEntries(Object.entries(obj).filter((e) => e[1] !== undefined));
25 |
26 | const results = {
27 | project: [],
28 | resolved: [],
29 | unresolved: [],
30 | };
31 | Object.entries(lockFile.packages).forEach(([name, p]) => {
32 | const bucket = p.name?.startsWith("@openubl-ui")
33 | ? results.project
34 | : p.resolved
35 | ? results.resolved
36 | : results.unresolved;
37 |
38 | bucket.push(
39 | removeUndefined({
40 | name,
41 | version: p.version,
42 | resolved: p.resolved,
43 | packageName: p.name,
44 | })
45 | );
46 | });
47 |
48 | // log findings
49 | toLog.packages = results.project;
50 | toLog.dependencies = {
51 | countResolved: results.resolved.length,
52 | countUnresolved: results.unresolved.length,
53 | unresolved: results.unresolved,
54 | };
55 |
56 | console.log(`package-lock.json (${lockFilePath}) status:`);
57 | console.dir(toLog, { depth: 3 });
58 | console.log();
59 | if (results.unresolved.length === 0) {
60 | console.log("\u{1f600} lock file is ok!");
61 | } else {
62 | console.log("\u{1f621} lock file contains unresolved dependencies!");
63 | }
64 |
65 | // exit the script with an appropriate error code
66 | process.exit(results.unresolved.length === 0 ? 0 : 1);
67 |
--------------------------------------------------------------------------------
/ui/client/config/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { JestConfigWithTsJest } from "ts-jest";
2 |
3 | // For a detailed explanation regarding each configuration property, visit:
4 | // https://jestjs.io/docs/en/configuration.html
5 |
6 | const config: JestConfigWithTsJest = {
7 | // Automatically clear mock calls and instances between every test
8 | clearMocks: true,
9 |
10 | // Indicates whether the coverage information should be collected while executing the test
11 | collectCoverage: false,
12 |
13 | // The directory where Jest should output its coverage files
14 | coverageDirectory: "coverage",
15 |
16 | // Stub out resources and provide handling for tsconfig.json paths
17 | moduleNameMapper: {
18 | // stub out files that don't matter for tests
19 | "\\.(css|less)$": "/__mocks__/styleMock.js",
20 | "\\.(xsd)$": "/__mocks__/styleMock.js",
21 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
22 | "/__mocks__/fileMock.js",
23 | "@patternfly/react-icons/dist/esm/icons/":
24 | "/__mocks__/fileMock.js",
25 |
26 | // match the paths in tsconfig.json
27 | "@app/(.*)": "/src/app/$1",
28 | "@assets/(.*)":
29 | "../node_modules/@patternfly/react-core/dist/styles/assets/$1",
30 | },
31 |
32 | // A list of paths to directories that Jest should use to search for files
33 | roots: ["/src"],
34 |
35 | // The test environment that will be used for testing
36 | testEnvironment: "jest-environment-jsdom",
37 |
38 | // The pattern or patterns Jest uses to find test files
39 | testMatch: ["/src/**/*.{test,spec}.{js,jsx,ts,tsx}"],
40 |
41 | // Process js/jsx/mjs/mjsx/ts/tsx/mts/mtsx with `ts-jest`
42 | transform: {
43 | "^.+\\.(js|mjs|ts|mts)x?$": "ts-jest",
44 | },
45 |
46 | // Code to set up the testing framework before each test file in the suite is executed
47 | setupFilesAfterEnv: ["/src/app/setupTests.ts"],
48 | };
49 |
50 | export default config;
51 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/ConfirmDialog.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Button,
4 | Modal,
5 | ButtonVariant,
6 | ModalVariant,
7 | } from "@patternfly/react-core";
8 |
9 | export interface ConfirmDialogProps {
10 | isOpen: boolean;
11 |
12 | title: string;
13 | titleIconVariant?:
14 | | "success"
15 | | "danger"
16 | | "warning"
17 | | "info"
18 | | React.ComponentType;
19 | message: string | React.ReactNode;
20 |
21 | confirmBtnLabel: string;
22 | cancelBtnLabel: string;
23 |
24 | inProgress?: boolean;
25 | confirmBtnVariant: ButtonVariant;
26 |
27 | onClose: () => void;
28 | onConfirm: () => void;
29 | onCancel?: () => void;
30 | }
31 |
32 | export const ConfirmDialog: React.FC = ({
33 | isOpen,
34 | title,
35 | titleIconVariant,
36 | message,
37 | confirmBtnLabel,
38 | cancelBtnLabel,
39 | inProgress,
40 | confirmBtnVariant,
41 | onClose,
42 | onConfirm,
43 | onCancel,
44 | }) => {
45 | const confirmBtn = (
46 |
56 | );
57 |
58 | const cancelBtn = onCancel ? (
59 |
69 | ) : undefined;
70 |
71 | return (
72 |
82 | {message}
83 |
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/deploy/compose/compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | postgres:
4 | image: docker.io/library/postgres:16
5 | ports:
6 | - "5432:5432"
7 | environment:
8 | POSTGRES_DB: "openubl"
9 | POSTGRES_USER: "user"
10 | POSTGRES_PASSWORD: "password"
11 | healthcheck:
12 | test: [ "CMD-SHELL", "pg_isready -U user -d openubl" ]
13 | interval: 5s
14 | timeout: 3s
15 | retries: 3
16 |
17 | # keycloak:
18 | # image: quay.io/keycloak/keycloak:23.0.3
19 | # command: start-dev
20 | # ports:
21 | # - "9001:8080"
22 | # environment:
23 | # KEYCLOAK_ADMIN: admin
24 | # KEYCLOAK_ADMIN_PASSWORD: admin
25 | # keycloak-init:
26 | # image: quay.io/keycloak/keycloak:23.0.3
27 | # entrypoint: /usr/bin/bash
28 | # command: /tmp/keycloak/setup.sh
29 | # environment:
30 | # KEYCLOAK_SERVER_URL: http://keycloak:8080
31 | # KEYCLOAK_ADMIN: admin
32 | # KEYCLOAK_ADMIN_PASSWORD: admin
33 | # volumes:
34 | # - ./scripts/keycloak:/tmp/keycloak:z
35 | # depends_on:
36 | # keycloak:
37 | # condition: service_started
38 | #
39 | # minio:
40 | # image: quay.io/minio/minio:latest
41 | # command: server --console-address ":9001" /data
42 | # ports:
43 | # - "9002:9000"
44 | # - "9003:9001"
45 | # environment:
46 | # MINIO_ROOT_USER: "admin"
47 | # MINIO_ROOT_PASSWORD: "password"
48 | # MINIO_NOTIFY_NATS_ENABLE_OPENUBL: "on"
49 | # MINIO_NOTIFY_NATS_ADDRESS_OPENUBL: "nats:4222"
50 | # MINIO_NOTIFY_NATS_SUBJECT_OPENUBL: "openubl"
51 | # healthcheck:
52 | # test: timeout 5s bash -c ':> /dev/tcp/127.0.0.1/9000' || exit 1
53 | # interval: 10s
54 | # timeout: 5s
55 | # retries: 5
56 | # depends_on:
57 | # nats:
58 | # condition: service_started
59 | # minio-init:
60 | # image: quay.io/minio/minio:latest
61 | # entrypoint: /usr/bin/bash
62 | # command: /tmp/minio/setup.sh
63 | # volumes:
64 | # - ./scripts/minio:/tmp/minio:z
65 | # depends_on:
66 | # minio:
67 | # condition: service_healthy
--------------------------------------------------------------------------------
/common/src/config.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fmt::{Display, Formatter},
3 | path::PathBuf,
4 | };
5 |
6 | #[derive(Debug, Clone, clap::ValueEnum)]
7 | pub enum DatabaseProvider {
8 | Sqlite,
9 | Postgresql,
10 | }
11 |
12 | impl Display for DatabaseProvider {
13 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
14 | match self {
15 | DatabaseProvider::Sqlite => write!(f, "sqlite"),
16 | DatabaseProvider::Postgresql => write!(f, "postgresql"),
17 | }
18 | }
19 | }
20 |
21 | #[derive(clap::Args, Debug, Clone)]
22 | #[command(next_help_heading = "Database")]
23 | pub struct Database {
24 | #[arg(
25 | id = "db-provider",
26 | long,
27 | env = "DB_PROVIDER",
28 | requires_ifs([("postgresql", "db-user"), ("postgresql", "db-password"), ("postgresql", "db-host"), ("postgresql", "db-port"), ("postgresql", "db-name")]),
29 | default_value_t = DatabaseProvider::Sqlite
30 | )]
31 | pub db_provider: DatabaseProvider,
32 |
33 | #[arg(
34 | id = "db-fs-path",
35 | long,
36 | env = "DB_FS_PATH",
37 | conflicts_with = "postgresql",
38 | help_heading = "Sqlite"
39 | )]
40 | pub fs_path: Option,
41 |
42 | #[command(flatten)]
43 | pub db_config: DatabaseConfig,
44 | }
45 |
46 | #[derive(Clone, Debug, Default, clap::Args)]
47 | #[command(next_help_heading = "Postgresql")]
48 | #[group(id = "postgresql", requires = "db-provider")]
49 | pub struct DatabaseConfig {
50 | #[arg(id = "db-user", long, env = "DB_USER", default_value = "postgres")]
51 | pub username: String,
52 | #[arg(
53 | id = "db-password",
54 | long,
55 | env = "DB_PASSWORD",
56 | default_value = "password"
57 | )]
58 | pub password: String,
59 | #[arg(id = "db-host", long, env = "DB_HOST", default_value = "localhost")]
60 | pub host: String,
61 | #[arg(id = "db-port", long, env = "DB_PORT", default_value_t = 5432)]
62 | pub port: u16,
63 | #[arg(id = "db-name", long, env = "DB_NAME", default_value = "openubl")]
64 | pub db_name: String,
65 | }
66 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/HookFormPFFields/HookFormPFSelect.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { FieldValues, Path } from "react-hook-form";
3 | import { FormSelect, FormSelectProps } from "@patternfly/react-core";
4 | import { getValidatedFromErrors } from "@app/utils/utils";
5 | import {
6 | extractGroupControllerProps,
7 | HookFormPFGroupController,
8 | BaseHookFormPFGroupControllerProps,
9 | } from "./HookFormPFGroupController";
10 |
11 | export type HookFormPFSelectProps<
12 | TFieldValues extends FieldValues,
13 | TName extends Path,
14 | > = FormSelectProps &
15 | BaseHookFormPFGroupControllerProps & {
16 | children: React.ReactNode;
17 | };
18 |
19 | export const HookFormPFSelect = <
20 | TFieldValues extends FieldValues = FieldValues,
21 | TName extends Path = Path,
22 | >(
23 | props: HookFormPFSelectProps
24 | ) => {
25 | const { extractedProps, remainingProps } = extractGroupControllerProps<
26 | TFieldValues,
27 | TName,
28 | HookFormPFSelectProps
29 | >(props);
30 | const { fieldId, helperText, isRequired, errorsSuppressed } = extractedProps;
31 | const { children, ref, ...rest } = remainingProps;
32 |
33 | return (
34 |
35 | {...extractedProps}
36 | renderInput={({
37 | field: { onChange, onBlur, value, name, ref },
38 | fieldState: { isDirty, error, isTouched },
39 | }) => (
40 |
56 | {props.children}
57 |
58 | )}
59 | />
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/cli/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::fs::create_dir_all;
3 | use std::process::{ExitCode, Termination};
4 |
5 | use clap::Parser;
6 | use tokio::task::{spawn_local, LocalSet};
7 |
8 | #[allow(clippy::large_enum_variant)]
9 | #[derive(clap::Subcommand, Debug)]
10 | pub enum Command {
11 | Server(openubl_server::ServerRun),
12 | }
13 |
14 | #[derive(clap::Parser, Debug)]
15 | #[command(
16 | author,
17 | version = env ! ("CARGO_PKG_VERSION"),
18 | about = "openubl",
19 | long_about = None
20 | )]
21 | pub struct Cli {
22 | #[command(subcommand)]
23 | pub(crate) command: Option,
24 | }
25 |
26 | impl Cli {
27 | async fn run(self) -> anyhow::Result {
28 | match self.command {
29 | Some(Command::Server(run)) => run.run().await,
30 | None => dev_mode().await,
31 | }
32 | }
33 | }
34 |
35 | async fn dev_mode() -> anyhow::Result {
36 | log::warn!("Setting up managed DB; not suitable for production use!");
37 |
38 | let current_dir = env::current_dir()?;
39 | let work_dir = current_dir.join(".openubl");
40 | let db_dir = work_dir.join("db");
41 | let data_dir = work_dir.join("data");
42 | create_dir_all(&db_dir)?;
43 | create_dir_all(&data_dir)?;
44 |
45 | let provider = "sqlite";
46 | let db_path = db_dir.join("db.sqlite").display().to_string();
47 | log::info!("Database installed in {:?}", db_dir);
48 |
49 | let api = Cli::parse_from([
50 | "cli",
51 | "server",
52 | "--db-provider",
53 | provider,
54 | "--db-fs-path",
55 | &db_path,
56 | "local",
57 | "--storage-local-dir",
58 | ".openubl/storage",
59 | ]);
60 |
61 | LocalSet::new()
62 | .run_until(async { spawn_local(api.run()).await? })
63 | .await
64 | }
65 |
66 | #[actix_web::main]
67 | async fn main() -> impl Termination {
68 | Cli::parse().run().await
69 | }
70 |
71 | #[cfg(test)]
72 | mod tests {
73 | use super::*;
74 |
75 | #[test]
76 | fn verify_cli() {
77 | use clap::CommandFactory;
78 | Cli::command().debug_assert();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/ui/client/src/mocks/config.ts:
--------------------------------------------------------------------------------
1 | import { ENV } from "@app/env";
2 |
3 | // Samples of what mock string to parse:
4 | // MOCK=
5 | // MOCK=off
6 | // MOCK=+pass
7 | // MOCK=full
8 | // MOCK=-full
9 | // MOCK=stub
10 | // MOCK=stub=*
11 | // MOCK=stub=*.pass
12 | // MOCK=stub=1,2,3.pass
13 | // MOCK=stub=1,2,3.+pass
14 | // MOCK=stub=1,2,3.-pass
15 |
16 | /**
17 | * Parse the provided MOCK configuration string and return a configuration object.
18 | */
19 | export function parseMock(str?: string): {
20 | enabled: boolean;
21 | passthrough: boolean;
22 | stub: boolean | "*" | string[];
23 | full: boolean;
24 | } {
25 | const regexOff = /^(off)?$/;
26 | const regexPassthrough = /^([+-]?)pass(through)?$/;
27 | const regexFull = /^([+-]?)full$/;
28 | const regexStub = /^stub(=\*|=([a-z0-9\-_]+(\s*,\s*[a-z0-9\-_]+)*))?$/;
29 |
30 | let off = !str;
31 | let passthrough = false;
32 | let full = false;
33 | let stub: boolean | "*" | string[] = false;
34 |
35 | str
36 | ?.toLowerCase()
37 | .split(".")
38 | .map((p) => p.trim())
39 | .forEach((part) => {
40 | if (part.match(regexOff)) {
41 | off = true;
42 | }
43 |
44 | const matchPassthrough = part.match(regexPassthrough);
45 | if (matchPassthrough) {
46 | passthrough =
47 | matchPassthrough[1].length === 0 || matchPassthrough[1] === "+";
48 | }
49 |
50 | const matchFull = part.match(regexFull);
51 | if (matchFull) {
52 | full = matchFull[1].length === 0 || matchFull[1] === "+";
53 | }
54 |
55 | const matchStub = part.match(regexStub);
56 | if (matchStub) {
57 | if (!matchStub[1] || matchStub[1] === "" || matchStub[1] === "=*") {
58 | stub = "*";
59 | } else {
60 | stub = matchStub[2].split(",").map((s) => s.trim());
61 | }
62 | }
63 | });
64 |
65 | return {
66 | passthrough,
67 | stub,
68 | full,
69 | enabled: !off && (passthrough || full || !!stub),
70 | };
71 | }
72 |
73 | export const config = Object.freeze(parseMock(ENV.MOCK));
74 | if (ENV.NODE_ENV === "development") {
75 | console.info("MOCK configuration: ", config);
76 | }
77 |
78 | export default config;
79 |
--------------------------------------------------------------------------------
/entity/src/delivery.rs:
--------------------------------------------------------------------------------
1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
2 |
3 | use sea_orm::entity::prelude::*;
4 |
5 | #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
6 | #[sea_orm(rs_type = "String", db_type = "String(StringLen::N(1))")]
7 | pub enum TargetSendFileProtocolAction {
8 | #[sea_orm(string_value = "B")]
9 | SoapBill,
10 | #[sea_orm(string_value = "S")]
11 | SoapSummary,
12 | #[sea_orm(string_value = "P")]
13 | SoapPack,
14 | #[sea_orm(string_value = "D")]
15 | RestSendDocument,
16 | }
17 |
18 | #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
19 | #[sea_orm(rs_type = "String", db_type = "String(StringLen::N(1))")]
20 | pub enum TargetVerifyTicketProtocolAction {
21 | #[sea_orm(string_value = "S")]
22 | SOAP,
23 | #[sea_orm(string_value = "R")]
24 | REST,
25 | }
26 |
27 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
28 | #[sea_orm(table_name = "delivery")]
29 | pub struct Model {
30 | #[sea_orm(primary_key)]
31 | pub id: i32,
32 | pub response_ticket: Option,
33 | pub response_cdr_description: Option,
34 | pub response_cdr_response_code: Option,
35 | // pub response_cdr_notes: Option>,
36 | pub response_error_code: Option,
37 | pub response_error_message: Option,
38 |
39 | pub target_send_file_protocol: TargetSendFileProtocolAction,
40 | pub target_send_file_url: String,
41 |
42 | pub target_verify_ticket_protocol: Option,
43 | pub target_verify_ticket_url: Option,
44 |
45 | pub document_id: i32,
46 | }
47 |
48 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
49 | pub enum Relation {
50 | #[sea_orm(
51 | belongs_to = "super::document::Entity",
52 | from = "Column::DocumentId",
53 | to = "super::document::Column::Id",
54 | on_update = "NoAction",
55 | on_delete = "Cascade"
56 | )]
57 | Document,
58 | }
59 |
60 | impl Related for Entity {
61 | fn to() -> RelationDef {
62 | Relation::Document.def()
63 | }
64 | }
65 |
66 | impl ActiveModelBehavior for ActiveModel {}
67 |
--------------------------------------------------------------------------------
/migration/src/m20240114_154538_create_credentials.rs:
--------------------------------------------------------------------------------
1 | use sea_orm_migration::prelude::*;
2 |
3 | #[derive(DeriveMigrationName)]
4 | pub struct Migration;
5 |
6 | #[async_trait::async_trait]
7 | impl MigrationTrait for Migration {
8 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9 | manager
10 | .create_table(
11 | Table::create()
12 | .table(Credentials::Table)
13 | .if_not_exists()
14 | .col(
15 | ColumnDef::new(Credentials::Id)
16 | .integer()
17 | .not_null()
18 | .auto_increment()
19 | .primary_key(),
20 | )
21 | .col(ColumnDef::new(Credentials::Name).string().not_null())
22 | .col(ColumnDef::new(Credentials::Description).string().null())
23 | .col(ColumnDef::new(Credentials::UsernameSol).string().not_null())
24 | .col(ColumnDef::new(Credentials::PasswordSol).string().not_null())
25 | .col(ColumnDef::new(Credentials::ClientId).string().not_null())
26 | .col(
27 | ColumnDef::new(Credentials::ClientSecret)
28 | .string()
29 | .not_null(),
30 | )
31 | .col(ColumnDef::new(Credentials::UrlInvoice).string().not_null())
32 | .col(ColumnDef::new(Credentials::UrlDespatch).string().not_null())
33 | .col(
34 | ColumnDef::new(Credentials::UrlPerceptionRetention)
35 | .string()
36 | .not_null(),
37 | )
38 | .to_owned(),
39 | )
40 | .await
41 | }
42 |
43 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
44 | manager
45 | .drop_table(Table::drop().table(Credentials::Table).to_owned())
46 | .await
47 | }
48 | }
49 |
50 | #[derive(DeriveIden)]
51 | pub enum Credentials {
52 | Table,
53 | Id,
54 | Name,
55 | Description,
56 | UrlInvoice,
57 | UrlDespatch,
58 | UrlPerceptionRetention,
59 | UsernameSol,
60 | PasswordSol,
61 | ClientId,
62 | ClientSecret,
63 | }
64 |
--------------------------------------------------------------------------------
/server/src/server/mod.rs:
--------------------------------------------------------------------------------
1 | use std::borrow::Cow;
2 | use std::fmt::Display;
3 |
4 | use actix_web::body::BoxBody;
5 | use actix_web::http::StatusCode;
6 | use actix_web::{HttpResponse, ResponseError};
7 |
8 | use openubl_api::system;
9 | use openubl_storage::StorageSystemErr;
10 |
11 | pub mod credentials;
12 | pub mod document;
13 | pub mod health;
14 |
15 | #[derive(Debug, thiserror::Error)]
16 | pub enum Error {
17 | #[error(transparent)]
18 | System(system::error::Error),
19 | #[error(transparent)]
20 | Io(std::io::Error),
21 | #[error(transparent)]
22 | Storage(#[from] StorageSystemErr),
23 | #[error("Invalid request {msg:?}")]
24 | BadRequest { msg: String, status: StatusCode },
25 | #[error(transparent)]
26 | Any(#[from] anyhow::Error),
27 | }
28 |
29 | impl From for Error {
30 | fn from(e: system::error::Error) -> Self {
31 | Self::System(e)
32 | }
33 | }
34 |
35 | impl From for Error {
36 | fn from(e: std::io::Error) -> Self {
37 | Self::Io(e)
38 | }
39 | }
40 |
41 | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
42 | pub struct ErrorInformation {
43 | pub r#type: Cow<'static, str>,
44 | pub message: String,
45 | }
46 |
47 | impl ErrorInformation {
48 | pub fn new(r#type: impl Into>, message: impl Display) -> Self {
49 | Self {
50 | r#type: r#type.into(),
51 | message: message.to_string(),
52 | }
53 | }
54 | }
55 |
56 | impl ResponseError for Error {
57 | fn error_response(&self) -> HttpResponse {
58 | match self {
59 | Self::System(err) => {
60 | HttpResponse::InternalServerError().json(ErrorInformation::new("System", err))
61 | }
62 | Self::Io(err) => {
63 | HttpResponse::InternalServerError().json(ErrorInformation::new("System IO", err))
64 | }
65 | Self::Storage(err) => HttpResponse::InternalServerError()
66 | .json(ErrorInformation::new("System storage", err)),
67 | Self::BadRequest { msg, status } => {
68 | HttpResponse::build(*status).json(ErrorInformation::new("Bad request", msg))
69 | }
70 | Self::Any(err) => HttpResponse::InternalServerError()
71 | .json(ErrorInformation::new("System unknown", err)),
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/ui/client/src/app/components/HookFormPFFields/HookFormPFTextInput.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { FieldValues, Path, PathValue } from "react-hook-form";
3 | import { TextInput, TextInputProps } from "@patternfly/react-core";
4 | import { getValidatedFromErrors } from "@app/utils/utils";
5 | import {
6 | extractGroupControllerProps,
7 | HookFormPFGroupController,
8 | BaseHookFormPFGroupControllerProps,
9 | } from "./HookFormPFGroupController";
10 |
11 | export type HookFormPFTextInputProps<
12 | TFieldValues extends FieldValues,
13 | TName extends Path,
14 | > = TextInputProps & BaseHookFormPFGroupControllerProps;
15 |
16 | export const HookFormPFTextInput = <
17 | TFieldValues extends FieldValues = FieldValues,
18 | TName extends Path = Path,
19 | >(
20 | props: HookFormPFTextInputProps
21 | ) => {
22 | const { extractedProps, remainingProps } = extractGroupControllerProps<
23 | TFieldValues,
24 | TName,
25 | HookFormPFTextInputProps
26 | >(props);
27 | const { fieldId, helperText, isRequired, errorsSuppressed } = extractedProps;
28 | const { type } = remainingProps;
29 | return (
30 |
31 | {...extractedProps}
32 | renderInput={({
33 | field: { onChange, onBlur, value, name, ref },
34 | fieldState: { isDirty, error, isTouched },
35 | }) => (
36 | {
43 | if (type === "number") {
44 | onChange(
45 | ((value && parseInt(value, 10)) || "") as PathValue<
46 | TFieldValues,
47 | TName
48 | >
49 | );
50 | } else {
51 | onChange(value as PathValue);
52 | }
53 | }}
54 | onBlur={onBlur}
55 | value={value}
56 | validated={
57 | errorsSuppressed
58 | ? "default"
59 | : getValidatedFromErrors(error, isDirty, isTouched)
60 | }
61 | {...remainingProps}
62 | />
63 | )}
64 | />
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/ui/common/src/environment.ts:
--------------------------------------------------------------------------------
1 | /** Define process.env to contain `OpenublEnvType` */
2 | declare global {
3 | // eslint-disable-next-line @typescript-eslint/no-namespace
4 | namespace NodeJS {
5 | interface ProcessEnv extends Partial> {}
6 | }
7 | }
8 |
9 | /**
10 | * The set of environment variables used by `@konveyor-ui` packages.
11 | */
12 | export type OpenublEnvType = {
13 | NODE_ENV: "development" | "production" | "test";
14 | VERSION: string;
15 |
16 | /** Controls how mock data is injected on the client */
17 | MOCK: string;
18 |
19 | /** Enable RBAC authentication/authorization */
20 | AUTH_REQUIRED: "true" | "false";
21 |
22 | /** SSO / Oidc client id */
23 | OIDC_CLIENT_ID: string;
24 |
25 | /** Branding to apply to the UI */
26 | PROFILE: "openubl" | "lf";
27 |
28 | /** UI upload file size limit in megabytes (MB), suffixed with "m" */
29 | UI_INGRESS_PROXY_BODY_SIZE: string;
30 |
31 | /** The listen port for the UI's server */
32 | PORT?: string;
33 |
34 | /** Target URL for the UI server's `/auth` proxy */
35 | OIDC_SERVER_URL?: string;
36 |
37 | /** Target URL for the UI server's `/hub` proxy */
38 | OPENUBL_HUB_URL?: string;
39 | };
40 |
41 | /**
42 | * Keys in `KonveyorEnvType` that are only used on the server and therefore do not
43 | * need to be sent to the client.
44 | */
45 | export const SERVER_ENV_KEYS = ["PORT", "OIDC_SERVER_URL", "OPENUBL_HUB_URL"];
46 |
47 | /**
48 | * Create a `KonveyorEnv` from a partial `KonveyorEnv` with a set of default values.
49 | */
50 | export const buildOpenublEnv = ({
51 | NODE_ENV = "production",
52 | PORT,
53 | VERSION = "99.0.0",
54 | MOCK = "off",
55 |
56 | OIDC_SERVER_URL,
57 | AUTH_REQUIRED = "false",
58 | OIDC_CLIENT_ID = "openubl-ui",
59 |
60 | PROFILE = "openubl",
61 | UI_INGRESS_PROXY_BODY_SIZE = "500m",
62 | OPENUBL_HUB_URL,
63 | }: Partial = {}): OpenublEnvType => ({
64 | NODE_ENV,
65 | PORT,
66 | VERSION,
67 | MOCK,
68 |
69 | OIDC_SERVER_URL,
70 | AUTH_REQUIRED,
71 | OIDC_CLIENT_ID,
72 |
73 | PROFILE,
74 | UI_INGRESS_PROXY_BODY_SIZE,
75 | OPENUBL_HUB_URL,
76 | });
77 |
78 | /**
79 | * Default values for `OpenublEnvType`.
80 | */
81 | export const OPENUBL_ENV_DEFAULTS = buildOpenublEnv();
82 |
83 | /**
84 | * Current `@openubl-ui` environment configurations from `process.env`.
85 | */
86 | export const OPENUBL_ENV = buildOpenublEnv(process.env);
87 |
--------------------------------------------------------------------------------
/ui/client/src/app/hooks/table-controls/getHubRequestParams.ts:
--------------------------------------------------------------------------------
1 | // Hub filter/sort/pagination utils
2 | // TODO these could use some unit tests!
3 |
4 | import { HubRequestParams } from "@app/api/models";
5 | import {
6 | serializeFilterRequestParamsForHub,
7 | getFilterHubRequestParams,
8 | IGetFilterHubRequestParamsArgs,
9 | } from "./getFilterHubRequestParams";
10 | import {
11 | serializeSortRequestParamsForHub,
12 | getSortHubRequestParams,
13 | IGetSortHubRequestParamsArgs,
14 | } from "./getSortHubRequestParams";
15 | import {
16 | serializePaginationRequestParamsForHub,
17 | getPaginationHubRequestParams,
18 | IGetPaginationHubRequestParamsArgs,
19 | } from "./getPaginationHubRequestParams";
20 |
21 | /**
22 | * Returns params required to fetch server-filtered/sorted/paginated data from the hub API.
23 | * - NOTE: This is Konveyor-specific.
24 | * - Takes "source of truth" state for all table features (returned by useTableControlState),
25 | * - Call after useTableControlState and before fetching API data and then calling useTableControlProps.
26 | * - Returns a HubRequestParams object which is structured for easier consumption by other code before the fetch is made.
27 | * @see useTableControlState
28 | * @see useTableControlProps
29 | */
30 | export const getHubRequestParams = <
31 | TItem,
32 | TSortableColumnKey extends string,
33 | TFilterCategoryKey extends string = string,
34 | >(
35 | args: IGetFilterHubRequestParamsArgs &
36 | IGetSortHubRequestParamsArgs &
37 | IGetPaginationHubRequestParamsArgs
38 | ): HubRequestParams => ({
39 | ...getFilterHubRequestParams(args),
40 | ...getSortHubRequestParams(args),
41 | ...getPaginationHubRequestParams(args),
42 | });
43 |
44 | /**
45 | * Converts the HubRequestParams object created above into URLSearchParams (the browser API object for URL query parameters).
46 | * - NOTE: This is Konveyor-specific.
47 | * - Used internally by the application's useFetch[Resource] hooks
48 | */
49 | export const serializeRequestParamsForHub = (
50 | deserializedParams: HubRequestParams
51 | ): URLSearchParams => {
52 | const serializedParams = new URLSearchParams();
53 | serializeFilterRequestParamsForHub(deserializedParams, serializedParams);
54 | serializeSortRequestParamsForHub(deserializedParams, serializedParams);
55 | serializePaginationRequestParamsForHub(deserializedParams, serializedParams);
56 | return serializedParams;
57 | };
58 |
--------------------------------------------------------------------------------
/ui/client/src/app/hooks/table-controls/getSortHubRequestParams.ts:
--------------------------------------------------------------------------------
1 | import { HubRequestParams } from "@app/api/models";
2 | import { SortState } from "@mturley-latest/react-table-batteries";
3 |
4 | /**
5 | * Args for getSortHubRequestParams
6 | * - Partially satisfied by the object returned by useTableControlState (ITableControlState)
7 | */
8 | export interface IGetSortHubRequestParamsArgs<
9 | TSortableColumnKey extends string,
10 | > {
11 | /**
12 | * The "source of truth" state for the sort feature (returned by usePaginationState)
13 | */
14 | sort?: SortState;
15 | /**
16 | * A map of `columnKey` values (keys of the `columnNames` object passed to useTableControlState) to the field keys used by the hub API for sorting on those columns
17 | * - Keys and values in this object will usually be the same, but sometimes we need to present a hub field with a different name/key or have a column that is a composite of multiple hub fields.
18 | */
19 | hubSortFieldKeys?: Record;
20 | }
21 |
22 | /**
23 | * Given the state for the sort feature and additional arguments, returns params the hub API needs to apply the current sort.
24 | * - Makes up part of the object returned by getHubRequestParams
25 | * @see getHubRequestParams
26 | */
27 | export const getSortHubRequestParams = ({
28 | sort: sortState,
29 | hubSortFieldKeys,
30 | }: IGetSortHubRequestParamsArgs): Partial => {
31 | if (!sortState?.activeSort || !hubSortFieldKeys) return {};
32 | const { activeSort } = sortState;
33 | return {
34 | sort: {
35 | field: hubSortFieldKeys[activeSort.columnKey],
36 | direction: activeSort.direction,
37 | },
38 | };
39 | };
40 |
41 | /**
42 | * Converts the values returned by getSortHubRequestParams into the URL query strings expected by the hub API
43 | * - Appends converted URL params to the given `serializedParams` object for use in the hub API request
44 | * - Constructs part of the object returned by serializeRequestParamsForHub
45 | * @see serializeRequestParamsForHub
46 | */
47 | export const serializeSortRequestParamsForHub = (
48 | deserializedParams: HubRequestParams,
49 | serializedParams: URLSearchParams
50 | ) => {
51 | const { sort } = deserializedParams;
52 | if (sort) {
53 | const { field, direction } = sort;
54 | serializedParams.append("sort", `${direction}:${field}`);
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/ui/client/config/webpack.prod.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import merge from "webpack-merge";
3 | import webpack, { Configuration } from "webpack";
4 | import MiniCssExtractPlugin from "mini-css-extract-plugin";
5 | import CssMinimizerPlugin from "css-minimizer-webpack-plugin";
6 | import HtmlWebpackPlugin from "html-webpack-plugin";
7 |
8 | import { OPENUBL_ENV } from "@openubl-ui/common";
9 | import { stylePaths } from "./stylePaths";
10 | import commonWebpackConfiguration from "./webpack.common";
11 |
12 | const brandType = OPENUBL_ENV.PROFILE;
13 | const pathTo = (relativePath: string) => path.resolve(__dirname, relativePath);
14 |
15 | const config = merge(commonWebpackConfiguration, {
16 | mode: "production",
17 | devtool: "nosources-source-map", // used to map stack traces on the client without exposing all of the source code
18 | output: {
19 | filename: "[name].[contenthash:8].min.js",
20 | chunkFilename: "js/[name].[chunkhash:8].min.js",
21 | assetModuleFilename: "assets/[name].[contenthash:8][ext]",
22 | },
23 |
24 | optimization: {
25 | minimize: true,
26 | minimizer: [
27 | "...", // The '...' string represents the webpack default TerserPlugin instance
28 | new CssMinimizerPlugin(),
29 | ],
30 | },
31 |
32 | module: {
33 | rules: [
34 | {
35 | test: /\.css$/,
36 | include: [...stylePaths],
37 | use: [MiniCssExtractPlugin.loader, "css-loader"],
38 | },
39 | {
40 | test: /\.yaml$/,
41 | use: "raw-loader",
42 | },
43 | ],
44 | },
45 |
46 | plugins: [
47 | new MiniCssExtractPlugin({
48 | filename: "[name].[contenthash:8].css",
49 | chunkFilename: "css/[name].[chunkhash:8].min.css",
50 | }),
51 | new CssMinimizerPlugin({
52 | minimizerOptions: {
53 | preset: ["default", { mergeLonghand: false }],
54 | },
55 | }),
56 | new webpack.EnvironmentPlugin({
57 | NODE_ENV: "production",
58 | }),
59 | // index.html generated at runtime via the express server to inject `_env`
60 | new HtmlWebpackPlugin({
61 | filename: "index.html.ejs",
62 | template: `!!raw-loader!${pathTo("../public/index.html.ejs")}`,
63 | favicon: pathTo(`../public/${brandType}-favicon.ico`),
64 | minify: {
65 | collapseWhitespace: false,
66 | keepClosingSlash: true,
67 | minifyJS: true,
68 | removeEmptyAttributes: true,
69 | removeRedundantAttributes: true,
70 | },
71 | }),
72 | ],
73 | });
74 |
75 | export default config;
76 |
--------------------------------------------------------------------------------
/ui/server/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | import path from "path";
4 | import { fileURLToPath } from "url";
5 |
6 | import express from "express";
7 | import ejs from "ejs";
8 | import cookieParser from "cookie-parser";
9 | import { createHttpTerminator } from "http-terminator";
10 | import { createProxyMiddleware } from "http-proxy-middleware";
11 |
12 | import {
13 | encodeEnv,
14 | OPENUBL_ENV,
15 | SERVER_ENV_KEYS,
16 | proxyMap,
17 | } from "@openubl-ui/common";
18 |
19 | const __dirname = fileURLToPath(new URL(".", import.meta.url));
20 | const pathToClientDist = path.join(__dirname, "../../client/dist");
21 |
22 | const brandType = OPENUBL_ENV.PROFILE;
23 | const port = parseInt(OPENUBL_ENV.PORT, 10) || 8080;
24 |
25 | const app = express();
26 | app.set("x-powered-by", false);
27 | app.use(cookieParser());
28 |
29 | // Setup proxy handling
30 | for (const proxyPath in proxyMap) {
31 | app.use(proxyPath, createProxyMiddleware(proxyMap[proxyPath]));
32 | }
33 |
34 | app.engine("ejs", ejs.renderFile);
35 | app.use(express.json());
36 | app.set("views", pathToClientDist);
37 | app.use(express.static(pathToClientDist));
38 |
39 | // Handle any request that hasn't already been handled by express.static or proxy
40 | app.get("*", (_, res) => {
41 | if (OPENUBL_ENV.NODE_ENV === "development") {
42 | res.send(`
43 |
44 | You're running in development mode! The UI is served by webpack-dev-server on port 9000: http://localhost:9000
45 | If you want to serve the UI via express to simulate production mode, run a full build with: npm run build
46 | and then in two separate terminals, run: npm run port-forward
and: npm run start
and the UI will be served on port 8080.
47 | `);
48 | } else {
49 | res.render("index.html.ejs", {
50 | _env: encodeEnv(OPENUBL_ENV, SERVER_ENV_KEYS),
51 | brandType,
52 | });
53 | }
54 | });
55 |
56 | // Start the server
57 | const server = app.listen(port, () => {
58 | console.log(`Server listening on port::${port}`);
59 | });
60 |
61 | // Handle shutdown signals Ctrl-C (SIGINT) and default podman/docker stop (SIGTERM)
62 | const httpTerminator = createHttpTerminator({ server });
63 |
64 | const shutdown = async (signal) => {
65 | if (!server) {
66 | console.log(`${signal}, no server running.`);
67 | return;
68 | }
69 |
70 | console.log(`${signal} - Stopping server on port::${port}`);
71 | await httpTerminator.terminate();
72 | console.log(`${signal} - Stopped server on port::${port}`);
73 | };
74 |
75 | process.on("SIGINT", shutdown);
76 | process.on("SIGTERM", shutdown);
77 |
--------------------------------------------------------------------------------
/server/src/lib.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Debug;
2 | use std::process::ExitCode;
3 | use std::sync::Arc;
4 |
5 | use actix_multipart::form::tempfile::TempFileConfig;
6 | use actix_web::middleware::Logger;
7 | use actix_web::{web, App, HttpServer};
8 |
9 | use openubl_api::system::InnerSystem;
10 | use openubl_common::config::Database;
11 | use openubl_storage::StorageSystem;
12 |
13 | use crate::server::credentials::{
14 | create_credentials, delete_credentials, get_credentials, list_credentials, update_credentials,
15 | };
16 | use crate::server::document::{get_document_file, list_documents, send_document};
17 | use crate::server::health;
18 |
19 | mod dto;
20 | pub mod server;
21 |
22 | /// Run the API server
23 | #[derive(clap::Args, Debug)]
24 | pub struct ServerRun {
25 | #[arg(short, long, env, default_value = "0.0.0.0:8080")]
26 | pub bind_addr: String,
27 |
28 | #[command(flatten)]
29 | pub database: Database,
30 |
31 | #[arg(long, env)]
32 | pub bootstrap: bool,
33 |
34 | #[command(subcommand)]
35 | pub storage: openubl_storage::config::Storage,
36 | }
37 |
38 | impl ServerRun {
39 | pub async fn run(self) -> anyhow::Result {
40 | env_logger::init();
41 |
42 | // Database
43 | let system = match self.bootstrap {
44 | true => InnerSystem::bootstrap(&self.database).await?,
45 | false => InnerSystem::with_config(&self.database).await?,
46 | };
47 |
48 | // Storage
49 | let storage = StorageSystem::new(&self.storage).await?;
50 |
51 | let app_state = Arc::new(AppState { system, storage });
52 |
53 | HttpServer::new(move || {
54 | App::new()
55 | .app_data(web::Data::from(app_state.clone()))
56 | .wrap(Logger::default())
57 | .app_data(TempFileConfig::default())
58 | .configure(configure)
59 | })
60 | .bind(self.bind_addr)?
61 | .run()
62 | .await?;
63 |
64 | Ok(ExitCode::SUCCESS)
65 | }
66 | }
67 |
68 | pub struct AppState {
69 | pub system: InnerSystem,
70 | pub storage: StorageSystem,
71 | }
72 |
73 | pub fn configure(config: &mut web::ServiceConfig) {
74 | // Health
75 | config.service(health::liveness);
76 | config.service(health::readiness);
77 |
78 | // Documents
79 | config.service(list_documents);
80 | config.service(get_document_file);
81 | config.service(send_document);
82 |
83 | // Credentials
84 | config.service(list_credentials);
85 | config.service(create_credentials);
86 | config.service(get_credentials);
87 | config.service(update_credentials);
88 | config.service(delete_credentials);
89 | }
90 |
--------------------------------------------------------------------------------
/ui/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | module.exports = {
4 | root: true,
5 |
6 | env: {
7 | browser: true,
8 | es2020: true,
9 | jest: true,
10 | },
11 |
12 | parser: "@typescript-eslint/parser",
13 | parserOptions: {
14 | ecmaFeatures: {
15 | jsx: true,
16 | },
17 | ecmaVersion: 2020, // keep in sync with tsconfig.json
18 | sourceType: "module",
19 | },
20 |
21 | // eslint-disable-next-line prettier/prettier
22 | extends: [
23 | "eslint:recommended",
24 | "plugin:@typescript-eslint/recommended",
25 | "plugin:react/recommended",
26 | "prettier",
27 | ],
28 |
29 | // eslint-disable-next-line prettier/prettier
30 | plugins: [
31 | "prettier",
32 | "unused-imports", // or eslint-plugin-import?
33 | "@typescript-eslint",
34 | "react",
35 | "react-hooks",
36 | "@tanstack/query",
37 | ],
38 |
39 | // NOTE: Tweak the rules as needed when bulk fixes get merged
40 | rules: {
41 | // TODO: set to "error" when prettier v2 to v3 style changes are fixed
42 | "prettier/prettier": ["warn"],
43 |
44 | // TODO: set to "error" when all resolved, but keep the `argsIgnorePattern`
45 | "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
46 |
47 | // TODO: each one of these can be removed or set to "error" when they're all resolved
48 | "unused-imports/no-unused-imports": ["warn"],
49 | "@typescript-eslint/ban-types": "warn",
50 | "@typescript-eslint/no-explicit-any": "warn",
51 | "react/jsx-key": "warn",
52 | "react-hooks/rules-of-hooks": "warn",
53 | "react-hooks/exhaustive-deps": "warn",
54 | "no-extra-boolean-cast": "warn",
55 | "prefer-const": "warn",
56 |
57 | // Allow the "cy-data" property for tackle-ui-test (but should really be "data-cy" w/o this rule)
58 | "react/no-unknown-property": ["error", { ignore: ["cy-data"] }],
59 |
60 | "@tanstack/query/exhaustive-deps": "error",
61 | "@tanstack/query/prefer-query-object-syntax": "error",
62 | },
63 |
64 | settings: {
65 | react: { version: "detect" },
66 | },
67 |
68 | ignorePatterns: [
69 | // don't ignore dot files so config files get linted
70 | "!.*.js",
71 | "!.*.cjs",
72 | "!.*.mjs",
73 |
74 | // take the place of `.eslintignore`
75 | "dist/",
76 | "generated/",
77 | "node_modules/",
78 | ],
79 |
80 | // this is a hack to make sure eslint will look at all of the file extensions we
81 | // care about without having to put it on the command line
82 | overrides: [
83 | {
84 | files: [
85 | "**/*.js",
86 | "**/*.jsx",
87 | "**/*.cjs",
88 | "**/*.mjs",
89 | "**/*.ts",
90 | "**/*.tsx",
91 | ],
92 | },
93 | ],
94 | };
95 |
--------------------------------------------------------------------------------
/ui/client/src/mocks/stub-new-work/projects.ts:
--------------------------------------------------------------------------------
1 | import { rest } from "msw";
2 |
3 | import * as AppRest from "@app/api/rest";
4 | import { Project } from "@app/api/models";
5 |
6 | function generateRandomId(min: number, max: number) {
7 | return Math.floor(Math.random() * (max - min + 1)) + min;
8 | }
9 |
10 | export const mockProjectArray: Project[] = [
11 | {
12 | id: 1,
13 | name: "Project 1",
14 | description: "Description for Project 1",
15 | },
16 | ];
17 |
18 | export const handlers = [
19 | // Commented out to avoid conflict with the real API
20 | rest.get(AppRest.PROJECTS, (req, res, ctx) => {
21 | return res(ctx.json(mockProjectArray));
22 | }),
23 | rest.get(`${AppRest.PROJECTS}/:id`, (req, res, ctx) => {
24 | const { id } = req.params;
25 | const mockProject = mockProjectArray.find(
26 | (app) => app.id === parseInt(id as string, 10)
27 | );
28 | if (mockProject) {
29 | return res(ctx.json(mockProject));
30 | } else {
31 | return res(ctx.status(404), ctx.json({ message: "Project not found" }));
32 | }
33 | }),
34 | rest.post(AppRest.PROJECTS, async (req, res, ctx) => {
35 | const newProject: Project = await req.json();
36 | newProject.id = generateRandomId(1000, 9999);
37 |
38 | const existingProjectIndex = mockProjectArray.findIndex(
39 | (elem) => elem.id === newProject.id
40 | );
41 |
42 | if (existingProjectIndex !== -1) {
43 | mockProjectArray[existingProjectIndex] = newProject;
44 | return res(
45 | ctx.status(200),
46 | ctx.json({ message: "Project updated successfully" })
47 | );
48 | } else {
49 | mockProjectArray.push(newProject);
50 | return res(
51 | ctx.status(201),
52 | ctx.json({ message: "Project created successfully" })
53 | );
54 | }
55 | }),
56 | rest.put(`${AppRest.PROJECTS}/:id`, async (req, res, ctx) => {
57 | const { id } = req.params;
58 | const newProject = await req.json();
59 |
60 | const existingProjectIndex = mockProjectArray.findIndex(
61 | (elem) => `${elem.id}` === `${id}`
62 | );
63 |
64 | if (existingProjectIndex !== -1) {
65 | mockProjectArray[existingProjectIndex] = newProject;
66 | return res(ctx.status(204));
67 | } else {
68 | return res(ctx.status(404));
69 | }
70 | }),
71 | rest.delete(`${AppRest.PROJECTS}/:id`, async (req, res, ctx) => {
72 | const { id } = req.params;
73 |
74 | const existingProjectIndex = mockProjectArray.findIndex(
75 | (elem) => `${elem.id}` === `${id}`
76 | );
77 |
78 | if (existingProjectIndex !== -1) {
79 | mockProjectArray.splice(existingProjectIndex, 1);
80 | return res(ctx.status(200));
81 | } else {
82 | return res(ctx.status(404));
83 | }
84 | }),
85 | ];
86 |
87 | export default handlers;
88 |
--------------------------------------------------------------------------------
/ui/client/src/app/queries/projects.ts:
--------------------------------------------------------------------------------
1 | import { AxiosError } from "axios";
2 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3 |
4 | import { New, Project } from "@app/api/models";
5 | import {
6 | createProject,
7 | deleteProject,
8 | getProjectById,
9 | getProjects,
10 | updateProject as updateProject,
11 | } from "@app/api/rest";
12 |
13 | export const ProjectsQueryKey = "projects";
14 |
15 | export const useFetchProjects = () => {
16 | const { data, isLoading, error, refetch } = useQuery({
17 | queryKey: [ProjectsQueryKey],
18 | queryFn: getProjects,
19 | });
20 | return {
21 | projects: data || [],
22 | isFetching: isLoading,
23 | fetchError: error,
24 | refetch,
25 | };
26 | };
27 |
28 | export const useFetchProjectById = (id?: number | string) => {
29 | const { data, isLoading, error } = useQuery({
30 | queryKey: [ProjectsQueryKey, id],
31 | queryFn: () =>
32 | id === undefined ? Promise.resolve(undefined) : getProjectById(id),
33 | enabled: id !== undefined,
34 | });
35 |
36 | return {
37 | project: data,
38 | isFetching: isLoading,
39 | fetchError: error as AxiosError,
40 | };
41 | };
42 |
43 | export const useCreateProjectMutation = (
44 | onSuccess: (res: Project) => void,
45 | onError: (err: AxiosError, payload: New) => void
46 | ) => {
47 | const queryClient = useQueryClient();
48 |
49 | return useMutation({
50 | mutationFn: createProject,
51 | onSuccess: ({ data }, _payload) => {
52 | onSuccess(data);
53 | queryClient.invalidateQueries({ queryKey: [ProjectsQueryKey] });
54 | },
55 | onError,
56 | });
57 | };
58 |
59 | export const useUpdateProjectMutation = (
60 | onSuccess: (payload: Project) => void,
61 | onError: (err: AxiosError, payload: Project) => void
62 | ) => {
63 | const queryClient = useQueryClient();
64 | return useMutation({
65 | mutationFn: updateProject,
66 | onSuccess: (_res, payload) => {
67 | onSuccess(payload);
68 | queryClient.invalidateQueries({ queryKey: [ProjectsQueryKey] });
69 | },
70 | onError: onError,
71 | });
72 | };
73 |
74 | export const useDeleteProjectMutation = (
75 | onSuccess: (id: number | string) => void,
76 | onError: (err: AxiosError, id: number | string) => void
77 | ) => {
78 | const queryClient = useQueryClient();
79 |
80 | const { isPending, mutate, error } = useMutation({
81 | mutationFn: deleteProject,
82 | onSuccess: (_res, id) => {
83 | onSuccess(id);
84 | queryClient.invalidateQueries({ queryKey: [ProjectsQueryKey] });
85 | },
86 | onError: (err: AxiosError, id) => {
87 | onError(err, id);
88 | queryClient.invalidateQueries({ queryKey: [ProjectsQueryKey] });
89 | },
90 | });
91 |
92 | return {
93 | mutate,
94 | isPending,
95 | error,
96 | };
97 | };
98 |
--------------------------------------------------------------------------------
/server/src/dto.rs:
--------------------------------------------------------------------------------
1 | use openubl_entity as entity;
2 | use serde::{Deserialize, Serialize};
3 |
4 | #[derive(Serialize, Deserialize)]
5 | pub struct DocumentDto {
6 | pub id: i32,
7 | pub supplier_id: String,
8 | pub document_id: String,
9 | pub document_type: String,
10 | pub voided_document_code: Option,
11 | }
12 |
13 | impl From for DocumentDto {
14 | fn from(value: entity::document::Model) -> Self {
15 | Self {
16 | id: value.id,
17 | supplier_id: value.supplier_id.clone(),
18 | document_id: value.identifier.clone(),
19 | document_type: value.r#type.clone(),
20 | voided_document_code: value.voided_document_code.clone(),
21 | }
22 | }
23 | }
24 |
25 | #[derive(Serialize, Deserialize)]
26 | pub struct CredentialsDto {
27 | pub id: i32,
28 | pub name: String,
29 | pub description: Option,
30 | pub username_sol: String,
31 | pub client_id: String,
32 | pub url_invoice: String,
33 | pub url_despatch: String,
34 | pub url_perception_retention: String,
35 | }
36 |
37 | #[derive(Serialize, Deserialize)]
38 | pub struct NewCredentialsDto {
39 | pub name: String,
40 | pub description: Option,
41 | pub username_sol: String,
42 | pub password_sol: String,
43 | pub client_id: String,
44 | pub client_secret: String,
45 | pub url_invoice: String,
46 | pub url_despatch: String,
47 | pub url_perception_retention: String,
48 | pub supplier_ids_applied_to: Vec,
49 | }
50 |
51 | impl From for CredentialsDto {
52 | fn from(value: entity::credentials::Model) -> Self {
53 | Self {
54 | id: value.id,
55 | name: value.name.clone(),
56 | description: value.description.clone(),
57 | username_sol: value.username_sol.clone(),
58 | client_id: value.client_id.clone(),
59 | url_invoice: value.url_invoice.clone(),
60 | url_despatch: value.url_despatch.clone(),
61 | url_perception_retention: value.url_perception_retention.clone(),
62 | }
63 | }
64 | }
65 |
66 | impl From for entity::credentials::Model {
67 | fn from(value: NewCredentialsDto) -> Self {
68 | Self {
69 | id: 0,
70 | name: value.name.clone(),
71 | description: value.description.clone(),
72 | username_sol: value.username_sol.clone(),
73 | password_sol: value.password_sol,
74 | client_id: value.client_id.clone(),
75 | client_secret: value.client_secret,
76 | url_invoice: value.url_invoice.clone(),
77 | url_despatch: value.url_despatch.clone(),
78 | url_perception_retention: value.url_perception_retention.clone(),
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/ui/client/src/app/layout/about.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import {
4 | AboutModal,
5 | Text,
6 | TextContent,
7 | TextList,
8 | TextListItem,
9 | TextVariants,
10 | } from "@patternfly/react-core";
11 |
12 | import openublBrandImage from "@app/images/Openubl-white-logo.svg";
13 | import lfBrandImage from "@app/images/Openubl-white-logo.svg";
14 |
15 | import backgroundImage from "@app/images/pfbg-icon.svg";
16 |
17 | import { APP_BRAND, BrandType } from "@app/Constants";
18 | import ENV from "@app/env";
19 |
20 | interface IButtonAboutAppProps {
21 | isOpen: boolean;
22 | onClose: () => void;
23 | }
24 |
25 | export const AboutApp: React.FC = ({
26 | isOpen,
27 | onClose,
28 | }) => {
29 | const brandName = APP_BRAND === BrandType.Openubl ? "Openubl" : "LibreFact";
30 |
31 | return (
32 |
43 |
44 | Acerca de
45 |
46 | {brandName} es una colección de herramientas que ayudan en la
47 | administración de documentos electrónicos basados en UBL (Universal
48 | Busissess Language) y los estándares de la SUNAT (Perú).
49 |
50 |
51 | {brandName} Permite crear, firmar, enviar, y consultar comprobantes
52 | electrónicos en la SUNAT
53 |
54 |
55 | {brandName} es un proyecto de{" "}
56 |
61 | Project OpenUBL
62 |
63 | .
64 |
65 |
66 | Para mayor información por favor diríjase a{" "}
67 |
72 | {brandName} documentación
73 |
74 | .
75 |
76 |
77 |
78 |
79 |
80 | Versión
81 | {ENV.VERSION}
82 |
83 |
84 |
85 |
86 | );
87 | };
88 |
--------------------------------------------------------------------------------
/api/src/db.rs:
--------------------------------------------------------------------------------
1 | use sea_orm::{
2 | ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr, ExecResult,
3 | QueryResult, Statement,
4 | };
5 | use serde::Deserialize;
6 |
7 | #[derive(Copy, Clone)]
8 | pub enum Transactional<'db> {
9 | None,
10 | Some(&'db DatabaseTransaction),
11 | }
12 |
13 | impl<'db> From<&'db DatabaseTransaction> for Transactional<'db> {
14 | fn from(inner: &'db DatabaseTransaction) -> Self {
15 | Self::Some(inner)
16 | }
17 | }
18 |
19 | #[derive(Clone)]
20 | pub enum ConnectionOrTransaction<'db> {
21 | Connection(&'db DatabaseConnection),
22 | Transaction(&'db DatabaseTransaction),
23 | }
24 |
25 | #[async_trait::async_trait]
26 | impl ConnectionTrait for ConnectionOrTransaction<'_> {
27 | fn get_database_backend(&self) -> DbBackend {
28 | match self {
29 | ConnectionOrTransaction::Connection(inner) => inner.get_database_backend(),
30 | ConnectionOrTransaction::Transaction(inner) => inner.get_database_backend(),
31 | }
32 | }
33 |
34 | async fn execute(&self, stmt: Statement) -> Result {
35 | match self {
36 | ConnectionOrTransaction::Connection(inner) => inner.execute(stmt).await,
37 | ConnectionOrTransaction::Transaction(inner) => inner.execute(stmt).await,
38 | }
39 | }
40 |
41 | async fn execute_unprepared(&self, sql: &str) -> Result {
42 | match self {
43 | ConnectionOrTransaction::Connection(inner) => inner.execute_unprepared(sql).await,
44 | ConnectionOrTransaction::Transaction(inner) => inner.execute_unprepared(sql).await,
45 | }
46 | }
47 |
48 | async fn query_one(&self, stmt: Statement) -> Result