From 2aba2ccf44c7a6d33c08e27b086eba2e8bf0952a Mon Sep 17 00:00:00 2001 From: Mahdi Baghbani Date: Tue, 19 Dec 2023 14:12:55 +0330 Subject: [PATCH] normalize: crlf to lf --- .github/workflows/audit.yaml | 116 ++-- .github/workflows/build.yaml | 160 +++--- .github/workflows/release.yaml | 244 ++++---- CODE_OF_CONDUCT.md | 272 ++++----- README.md | 10 +- .../technical decisions/authentication.md | 160 +++--- api/src/configs.rs | 138 ++--- api/src/configs/database.rs | 114 ++-- api/src/configs/jwt.rs | 76 +-- api/src/configs/openapi.rs | 138 ++--- api/src/constants.rs | 54 +- api/src/databases.rs | 36 +- api/src/middlewares.rs | 96 ++-- api/src/routers.rs | 140 ++--- api/src/routers/docs.rs | 82 +-- api/src/routers/healthcheck.rs | 24 +- api/src/routers/statics.rs | 16 +- api/src/schemas.rs | 74 +-- database/docs/setup.md | 34 +- deny.toml | 86 +-- docker/base.Dockerfile | 80 +-- frontend/public/app.webmanifest | 56 +- frontend/src/components/mod.rs | 6 +- frontend/src/constants.rs | 22 +- frontend/src/inits.rs | 186 +++---- frontend/src/models.rs | 70 +-- .../authentication/components/sign_in.rs | 520 +++++++++--------- frontend/src/pages/authentication/inits.rs | 16 +- frontend/src/pages/authentication/mod.rs | 10 +- frontend/src/pages/authentication/models.rs | 12 +- frontend/src/pages/authentication/updates.rs | 16 +- frontend/src/pages/authentication/urls.rs | 18 +- frontend/src/pages/authentication/views.rs | 24 +- frontend/src/pages/home/inits.rs | 136 ++--- frontend/src/pages/home/mod.rs | 12 +- frontend/src/pages/home/models.rs | 38 +- frontend/src/pages/home/updates.rs | 16 +- frontend/src/pages/home/urls.rs | 20 +- frontend/src/pages/home/views.rs | 30 +- frontend/src/updates.rs | 182 +++--- frontend/src/urls.rs | 66 +-- frontend/src/views.rs | 76 +-- 42 files changed, 1841 insertions(+), 1841 deletions(-) diff --git a/.github/workflows/audit.yaml b/.github/workflows/audit.yaml index 0fc7c94..f838017 100644 --- a/.github/workflows/audit.yaml +++ b/.github/workflows/audit.yaml @@ -1,58 +1,58 @@ -name: Audit - -on: - # Schedule daily updates. - schedule: [ { cron: "0 0 * * *" } ] - # (optional) Run workflow manually. - workflow_dispatch: - # (optional) Run workflow when pushing on master. - push: - paths: - # Run if workflow changes - - ".github/workflows/audit.yaml" - # Run on changed dependencies - - "**/Cargo.toml" - - "**/Cargo.lock" - # Run if the configuration file changes - - "**/audit.toml" - pull_request: - -permissions: read-all - -jobs: - general_audit: - runs-on: ubuntu-22.04 - strategy: - matrix: - checks: - - advisories - - bans licenses sources - - # Prevent sudden announcement of a new advisory from failing ci: - continue-on-error: ${{ matrix.checks == 'advisories' }} - - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - name: Checkout - uses: actions/checkout@v4 - - - uses: EmbarkStudios/cargo-deny-action@v1 - with: - log-level: error - command: check ${{ matrix.checks }} - arguments: --all-features - - security_audit: - runs-on: ubuntu-22.04 - permissions: - issues: write - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - name: Checkout - uses: actions/checkout@v4 - - - name: Audit Rust Dependencies - uses: actions-rust-lang/audit@v1 - with: - # Comma separated list of issues to ignore - ignore: RUSTSEC-2023-0071 +name: Audit + +on: + # Schedule daily updates. + schedule: [ { cron: "0 0 * * *" } ] + # (optional) Run workflow manually. + workflow_dispatch: + # (optional) Run workflow when pushing on master. + push: + paths: + # Run if workflow changes + - ".github/workflows/audit.yaml" + # Run on changed dependencies + - "**/Cargo.toml" + - "**/Cargo.lock" + # Run if the configuration file changes + - "**/audit.toml" + pull_request: + +permissions: read-all + +jobs: + general_audit: + runs-on: ubuntu-22.04 + strategy: + matrix: + checks: + - advisories + - bans licenses sources + + # Prevent sudden announcement of a new advisory from failing ci: + continue-on-error: ${{ matrix.checks == 'advisories' }} + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout + uses: actions/checkout@v4 + + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + log-level: error + command: check ${{ matrix.checks }} + arguments: --all-features + + security_audit: + runs-on: ubuntu-22.04 + permissions: + issues: write + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout + uses: actions/checkout@v4 + + - name: Audit Rust Dependencies + uses: actions-rust-lang/audit@v1 + with: + # Comma separated list of issues to ignore + ignore: RUSTSEC-2023-0071 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3c38633..765986a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,80 +1,80 @@ -name: Build - -on: - workflow_dispatch: - # (optional) Run workflow when pushing on master. - push: - pull_request: - -env: - CRATE_NAME: cyb - GITHUB_TOKEN: ${{ github.token }} - RUST_BACKTRACE: 1 - -jobs: - build-backend: - if: (startsWith( github.ref, 'refs/tags/v' ) || github.ref == 'refs/tags/test-release') != true - name: ${{ matrix.platform.os_name }} with rust ${{ matrix.toolchain }} - runs-on: ${{ matrix.platform.os }} - strategy: - fail-fast: false - matrix: - platform: - - os_name: Linux-x86_64 - os: ubuntu-22.04 - target: x86_64-unknown-linux-gnu - bin: cyb - name: cyb-Linux-x86_64-GNU.tar.gz - - - os_name: macOS-x86_64 - os: macOS-latest - target: x86_64-apple-darwin - bin: cyb - name: cyb-Darwin-x86_64.tar.gz - - - os_name: Windows-x86_64 - os: windows-latest - target: x86_64-pc-windows-msvc - bin: cyb.exe - name: cyb-Windows-x86_64.zip - toolchain: - - stable - # - beta - # - nightly - - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - name: Checkout - uses: actions/checkout@v4 - - - name: Cache cargo & target directories - uses: Swatinem/rust-cache@v2 - with: - key: "v2" - - - name: Configure Git - run: | - git config --global user.email "jdoe@example.com" - git config --global user.name "J. Doe" - - - name: Install musl-tools on Linux - run: sudo apt-get update --yes && sudo apt-get install --yes musl-tools - if: contains(matrix.platform.name, 'musl') - - - name: Build binary - uses: houseabsolute/actions-rust-cross@v0 - with: - command: "build" - target: ${{ matrix.platform.target }} - toolchain: ${{ matrix.toolchain }} - args: "--locked --release" - strip: true - - - name: Run tests - uses: houseabsolute/actions-rust-cross@v0 - with: - command: "test" - target: ${{ matrix.platform.target }} - toolchain: ${{ matrix.toolchain }} - args: "--locked --release" - if: ${{ !matrix.platform.skip_tests }} +name: Build + +on: + workflow_dispatch: + # (optional) Run workflow when pushing on master. + push: + pull_request: + +env: + CRATE_NAME: cyb + GITHUB_TOKEN: ${{ github.token }} + RUST_BACKTRACE: 1 + +jobs: + build-backend: + if: (startsWith( github.ref, 'refs/tags/v' ) || github.ref == 'refs/tags/test-release') != true + name: ${{ matrix.platform.os_name }} with rust ${{ matrix.toolchain }} + runs-on: ${{ matrix.platform.os }} + strategy: + fail-fast: false + matrix: + platform: + - os_name: Linux-x86_64 + os: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + bin: cyb + name: cyb-Linux-x86_64-GNU.tar.gz + + - os_name: macOS-x86_64 + os: macOS-latest + target: x86_64-apple-darwin + bin: cyb + name: cyb-Darwin-x86_64.tar.gz + + - os_name: Windows-x86_64 + os: windows-latest + target: x86_64-pc-windows-msvc + bin: cyb.exe + name: cyb-Windows-x86_64.zip + toolchain: + - stable + # - beta + # - nightly + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache cargo & target directories + uses: Swatinem/rust-cache@v2 + with: + key: "v2" + + - name: Configure Git + run: | + git config --global user.email "jdoe@example.com" + git config --global user.name "J. Doe" + + - name: Install musl-tools on Linux + run: sudo apt-get update --yes && sudo apt-get install --yes musl-tools + if: contains(matrix.platform.name, 'musl') + + - name: Build binary + uses: houseabsolute/actions-rust-cross@v0 + with: + command: "build" + target: ${{ matrix.platform.target }} + toolchain: ${{ matrix.toolchain }} + args: "--locked --release" + strip: true + + - name: Run tests + uses: houseabsolute/actions-rust-cross@v0 + with: + command: "test" + target: ${{ matrix.platform.target }} + toolchain: ${{ matrix.toolchain }} + args: "--locked --release" + if: ${{ !matrix.platform.skip_tests }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ce419df..c99f3f7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,122 +1,122 @@ -name: Release - -on: - workflow_dispatch: - # (optional) Run workflow when pushing on master. - push: - branches: - - "master" - pull_request: - branches: - - "master" - -env: - CRATE_NAME: cyb - GITHUB_TOKEN: ${{ github.token }} - RUST_BACKTRACE: 1 - -jobs: - release-backend: - if: startsWith( github.ref, 'refs/tags/v' ) || github.ref == 'refs/tags/test-release' - name: ${{ matrix.platform.os_name }} with rust ${{ matrix.toolchain }} - runs-on: ${{ matrix.platform.os }} - strategy: - fail-fast: false - matrix: - platform: - - os_name: Linux-x86_64 - os: ubuntu-22.04 - target: x86_64-unknown-linux-gnu - bin: cyb - name: cyb-Linux-x86_64-GNU.tar.gz - - - os_name: macOS-x86_64 - os: macOS-latest - target: x86_64-apple-darwin - bin: cyb - name: cyb-Darwin-x86_64.tar.gz - - - os_name: Windows-x86_64 - os: windows-latest - target: x86_64-pc-windows-msvc - bin: cyb.exe - name: cyb-Windows-x86_64.zip - toolchain: - - stable - # - beta - # - nightly - - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - name: Checkout - uses: actions/checkout@v4 - - - name: Cache cargo & target directories - uses: Swatinem/rust-cache@v2 - with: - key: "v2" - - - name: Configure Git - run: | - git config --global user.email "jdoe@example.com" - git config --global user.name "J. Doe" - - - name: Install musl-tools on Linux - run: sudo apt-get update --yes && sudo apt-get install --yes musl-tools - if: contains(matrix.platform.name, 'musl') - - - name: Build binary - uses: houseabsolute/actions-rust-cross@v0 - with: - command: "build" - target: ${{ matrix.platform.target }} - toolchain: ${{ matrix.toolchain }} - args: "--locked --release" - strip: true - - - name: Run tests - uses: houseabsolute/actions-rust-cross@v0 - with: - command: "test" - target: ${{ matrix.platform.target }} - toolchain: ${{ matrix.toolchain }} - args: "--locked --release" - if: ${{ !matrix.platform.skip_tests }} - - - name: Package as archive - shell: bash - run: | - cd target/${{ matrix.platform.target }}/release - if [[ "${{ matrix.platform.os }}" == "windows-latest" ]]; then - 7z a ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }} - else - tar czvf ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }} - fi - cd - - if: | - matrix.toolchain == 'stable' && - ( startsWith( github.ref, 'refs/tags/v' ) || - github.ref == 'refs/tags/test-release' ) - - - name: Publish release artifacts - uses: actions/upload-artifact@v3 - with: - name: cyb-${{ matrix.platform.os_name }} - path: "cyb-*" - if: matrix.toolchain == 'stable' && github.ref == 'refs/tags/test-release' - - - name: Generate SHA-256 - run: shasum -a 256 ${{ matrix.platform.name }} - if: | - matrix.toolchain == 'stable' && - matrix.platform.os == 'macOS-latest' && - ( startsWith( github.ref, 'refs/tags/v' ) || - github.ref == 'refs/tags/test-release' ) - - - name: Publish GitHub release - uses: softprops/action-gh-release@v1 - with: - draft: true - files: "cyb*" - body_path: Changes.md - if: matrix.toolchain == 'stable' && startsWith( github.ref, 'refs/tags/v' ) +name: Release + +on: + workflow_dispatch: + # (optional) Run workflow when pushing on master. + push: + branches: + - "master" + pull_request: + branches: + - "master" + +env: + CRATE_NAME: cyb + GITHUB_TOKEN: ${{ github.token }} + RUST_BACKTRACE: 1 + +jobs: + release-backend: + if: startsWith( github.ref, 'refs/tags/v' ) || github.ref == 'refs/tags/test-release' + name: ${{ matrix.platform.os_name }} with rust ${{ matrix.toolchain }} + runs-on: ${{ matrix.platform.os }} + strategy: + fail-fast: false + matrix: + platform: + - os_name: Linux-x86_64 + os: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + bin: cyb + name: cyb-Linux-x86_64-GNU.tar.gz + + - os_name: macOS-x86_64 + os: macOS-latest + target: x86_64-apple-darwin + bin: cyb + name: cyb-Darwin-x86_64.tar.gz + + - os_name: Windows-x86_64 + os: windows-latest + target: x86_64-pc-windows-msvc + bin: cyb.exe + name: cyb-Windows-x86_64.zip + toolchain: + - stable + # - beta + # - nightly + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache cargo & target directories + uses: Swatinem/rust-cache@v2 + with: + key: "v2" + + - name: Configure Git + run: | + git config --global user.email "jdoe@example.com" + git config --global user.name "J. Doe" + + - name: Install musl-tools on Linux + run: sudo apt-get update --yes && sudo apt-get install --yes musl-tools + if: contains(matrix.platform.name, 'musl') + + - name: Build binary + uses: houseabsolute/actions-rust-cross@v0 + with: + command: "build" + target: ${{ matrix.platform.target }} + toolchain: ${{ matrix.toolchain }} + args: "--locked --release" + strip: true + + - name: Run tests + uses: houseabsolute/actions-rust-cross@v0 + with: + command: "test" + target: ${{ matrix.platform.target }} + toolchain: ${{ matrix.toolchain }} + args: "--locked --release" + if: ${{ !matrix.platform.skip_tests }} + + - name: Package as archive + shell: bash + run: | + cd target/${{ matrix.platform.target }}/release + if [[ "${{ matrix.platform.os }}" == "windows-latest" ]]; then + 7z a ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }} + else + tar czvf ../../../${{ matrix.platform.name }} ${{ matrix.platform.bin }} + fi + cd - + if: | + matrix.toolchain == 'stable' && + ( startsWith( github.ref, 'refs/tags/v' ) || + github.ref == 'refs/tags/test-release' ) + + - name: Publish release artifacts + uses: actions/upload-artifact@v3 + with: + name: cyb-${{ matrix.platform.os_name }} + path: "cyb-*" + if: matrix.toolchain == 'stable' && github.ref == 'refs/tags/test-release' + + - name: Generate SHA-256 + run: shasum -a 256 ${{ matrix.platform.name }} + if: | + matrix.toolchain == 'stable' && + matrix.platform.os == 'macOS-latest' && + ( startsWith( github.ref, 'refs/tags/v' ) || + github.ref == 'refs/tags/test-release' ) + + - name: Publish GitHub release + uses: softprops/action-gh-release@v1 + with: + draft: true + files: "cyb*" + body_path: Changes.md + if: matrix.toolchain == 'stable' && startsWith( github.ref, 'refs/tags/v' ) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 41a6f0a..133e9c7 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,136 +1,136 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, caste, color, religion, or sexual -identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall - community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or advances of - any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, - without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official email address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -mahdi-baghbani@azadehafzar.io. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of -actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the -community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. - -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at -[https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org - -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html - -[Mozilla CoC]: https://github.com/mozilla/diversity - -[FAQ]: https://www.contributor-covenant.org/faq - -[translations]: https://www.contributor-covenant.org/translations +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +mahdi-baghbani@azadehafzar.io. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org + +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html + +[Mozilla CoC]: https://github.com/mozilla/diversity + +[FAQ]: https://www.contributor-covenant.org/faq + +[translations]: https://www.contributor-covenant.org/translations diff --git a/README.md b/README.md index b2d6369..653b340 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![Audit](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/audit.yaml/badge.svg)](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/audit.yaml) -[![Build](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/build.yaml/badge.svg)](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/build.yaml) -[![Release](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/release.yaml/badge.svg)](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/release.yaml) -[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) - +[![Audit](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/audit.yaml/badge.svg)](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/audit.yaml) +[![Build](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/build.yaml/badge.svg)](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/build.yaml) +[![Release](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/release.yaml/badge.svg)](https://github.com/MahdiBaghbani/connect-your-books/actions/workflows/release.yaml) +[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) + diff --git a/api/docs/technical decisions/authentication.md b/api/docs/technical decisions/authentication.md index d275d7f..997a4ee 100644 --- a/api/docs/technical decisions/authentication.md +++ b/api/docs/technical decisions/authentication.md @@ -1,80 +1,80 @@ -# Authentication - ---- - -1. Using **JWT** (JSON Web Token) for authentication over - **HTTP Basic Auth**. - -    This is from [stackoverflow](https://security.stackexchange.com/a/248209/164079)! -So, what are the advantages of JWTs over HTTP Basic Auth? In no particular order: - -* JWTs are generally specific to one service. If I take a JWT that tells StackExchange that I'm cbhacking and try to use - it on GitHub, there is no chance GitHub will consider this valid. Passwords (sadly) get reused all over. -* JWTs are time-limited, and expire. If you dig through a dump of some poorly-sanitized logs from months ago and find a - JWT I used, it will almost certainly (if the implementer had any sense) long since have expired and thus be basically - worthless. If you instead find a basic auth string, odds are pretty good that the password it contains is still valid. -* JWTs can be issued by one service and verified by another. This is one of the standard things they're used for, in - fact. If you log into a site A via "Sign in with Facebook" or some other SSO (Single Sign-On) provider B, the site A - that you're logging into will never see your password; that either goes straight to the SSO provider B, or did at some - time in the past (and your session with B is still active). Then B can return a JWT saying "The bearer of this is - Arjun, user ID 255431" and signed in a way that A can *verify* but not *create* (this is the magic of asymmetric - cryptography). Now site A knows who you are - so long as you send the JWT to them - without ever seeing any of your - credentials. -* JWTs can grant scoped permissions. Suppose I have a calendar online, and since it's my calendar, I can see all the - details of events, add and remove events, and so on. Now suppose some third-party app for scheduling calls with - clients wants to access my calendar. I want it to know what times I'm available, but I don't want it to know what I'm - doing during any particular slot. Maybe I'd let it add and remove those client meetings directly, but it shouldn't be - able to remove meetings (or dentist appointments, or vacations, or friends' birthdays) that I added for my own use ( - nor should it even be able to tell what those are). With basic auth, I'd have to set up multiple unique passwords - - one for each service (client meetings, friend birthdays, etc.) that I don't want to give full access to - or else - share the main password and then any app that wanted to could take total control. With a JWT, each app can request - only the permissions that it needs, and when I approve them it gets a JWT with "scope" claims that limit what the - token can be used for. -* JWTs can be both secure and fast to verify. Password storage, necessary for basic auth, can't. Symmetric signatures ( - HMACs) on JWTs are super-fast to verify, usually at most taking a few rounds of hashing. Asymmetric signatures are - significantly more expensive to verify, but you can still do them very quickly especially if you use efficient - algorithms, without compromising security. Verifying passwords, however, needs to be very slow indeed, because they're - too easy to brute-force from a leaked database if you use a fast hash or similar. Thus, password hashing is - deliberately designed to be computationally expensive - tens of thousands of rounds of hashing is one simple way to do - it - and is usually tuned to take somewhere between about 50ms and 250ms per user, on security-conscious sites. As - computers get faster, you have to make the password hash even more expensive, too, which isn't a significant concern - with JWT signatures. This means that, with basic auth, you have to either add a ton of CPU time spent doing extremely - expensive password hashing, or you have to run a very high risk that, if your password database is leaked, people will - be able to brute-force the hashes. With JWTs that's not nearly as big a concern (with HMAC-signed JWTs, it's basically - not a concern at all). -* Relatedly, JWTs can be verified without a database lookup at all. Passwords, including ones sent in basic auth, can't. - You can cache them, but that takes server-side resources and breaks down when you have a cluster of servers behind a - load balancer. Thus, JWTs are much better for scaling to large numbers of users, where DB hits are relatively - expensive and need to be minimized. -* Basic auth isn't really compatible with multi-factor authentication (sometimes called two-step authentication or - similar). MFA is usually implemented with ephemeral values - one-time codes generated in an app or sent via SMS, - challenge nonces sent to hardware tokens or push notifications, etc. - and even if there was a practical way to - include the resulting token in the basic auth string (there isn't, for browsers), it would become invalid almost - immediately. That's awkward if you want a session to last more than a few minutes before the user has to log in again! - With a JWT (or any other session token), you can verify the user's credentials (including MFA) once, and then give - them a token good for as long as you want (with JWTs in particular, it's often two tokens: the JWT itself which has a - short lifetime, and a refresh token that lasts longer but takes a DB lookup to verify). -* A bit of a technical curiosity rather than a deliberate feature of JWT design, but: HTTP Basic Auth is automatically - sent by the browser for any request to the relevant domain... even if the request comes from a different domain. This - means that there's a risk of CSRF (Cross-Site Request Forgery), where if site X thinks you might be signed in to site - Y, it can make your browser send requests to Y *and Y will think you, the user, did it*. This could be used for - anything from posting messages on a forum to transferring money between accounts, and it would happen without your - awareness. JWTs, by comparison, can be put in lots of different places. Cookies are common, and *used to be* - automatically sent even on requests from other domains (just like Basic Auth), but these days there's the `SameSite` - cookie flag that can be used to control this behavior. JWTs are also often sent in other HTTP request headers ( - often `Authorization` - the same one used for basic auth - but sometimes custom ones), and in those cases the - requesting client must explicitly specify to send the JWT. Which means the requesting client has to know the full - contents of the JWT, because your browser won't just automatically attach it to every relevant request. -* Logging out a user who uses a JWT is simple, just delete it from their client (this doesn't invalidate the JWT on the - server - that's hard to do, and usually you just rely on the JWT having a short lifetime and expiring soon - but it - works fine for Alice getting up from the computer before Bob sits down to use it). It turns out that there's no - standardized way to make a browser forget basic auth credentials. At least one browser (Internet Explorer) has a - special API for this, but generally speaking, if you want to log out a client that uses basic auth, the only option is - to tell the browser that their authorization is invalid (which generally makes the browser stop sending it). That's... - messy. Obviously for non-browser clients this is not an issue. -* Relatedly, logging in with basic auth isn't really under the control of the client application (implemented in - HTML/CSS/JS). The web browser chooses what to show (usually a pretty ugly modal credentials prompt). Other forms of - authentication / authorization token allow the web application to create their own login pages, which might include - features like a "forgot password?" link or SSO login options, while a basic auth site just presents the user with a - blank page (the content won't be loaded - or even requested - until the user signs in!) and an ugly pop-up modal box. - +# Authentication + +--- + +1. Using **JWT** (JSON Web Token) for authentication over + **HTTP Basic Auth**. + +    This is from [stackoverflow](https://security.stackexchange.com/a/248209/164079)! +So, what are the advantages of JWTs over HTTP Basic Auth? In no particular order: + +* JWTs are generally specific to one service. If I take a JWT that tells StackExchange that I'm cbhacking and try to use + it on GitHub, there is no chance GitHub will consider this valid. Passwords (sadly) get reused all over. +* JWTs are time-limited, and expire. If you dig through a dump of some poorly-sanitized logs from months ago and find a + JWT I used, it will almost certainly (if the implementer had any sense) long since have expired and thus be basically + worthless. If you instead find a basic auth string, odds are pretty good that the password it contains is still valid. +* JWTs can be issued by one service and verified by another. This is one of the standard things they're used for, in + fact. If you log into a site A via "Sign in with Facebook" or some other SSO (Single Sign-On) provider B, the site A + that you're logging into will never see your password; that either goes straight to the SSO provider B, or did at some + time in the past (and your session with B is still active). Then B can return a JWT saying "The bearer of this is + Arjun, user ID 255431" and signed in a way that A can *verify* but not *create* (this is the magic of asymmetric + cryptography). Now site A knows who you are - so long as you send the JWT to them - without ever seeing any of your + credentials. +* JWTs can grant scoped permissions. Suppose I have a calendar online, and since it's my calendar, I can see all the + details of events, add and remove events, and so on. Now suppose some third-party app for scheduling calls with + clients wants to access my calendar. I want it to know what times I'm available, but I don't want it to know what I'm + doing during any particular slot. Maybe I'd let it add and remove those client meetings directly, but it shouldn't be + able to remove meetings (or dentist appointments, or vacations, or friends' birthdays) that I added for my own use ( + nor should it even be able to tell what those are). With basic auth, I'd have to set up multiple unique passwords - + one for each service (client meetings, friend birthdays, etc.) that I don't want to give full access to - or else + share the main password and then any app that wanted to could take total control. With a JWT, each app can request + only the permissions that it needs, and when I approve them it gets a JWT with "scope" claims that limit what the + token can be used for. +* JWTs can be both secure and fast to verify. Password storage, necessary for basic auth, can't. Symmetric signatures ( + HMACs) on JWTs are super-fast to verify, usually at most taking a few rounds of hashing. Asymmetric signatures are + significantly more expensive to verify, but you can still do them very quickly especially if you use efficient + algorithms, without compromising security. Verifying passwords, however, needs to be very slow indeed, because they're + too easy to brute-force from a leaked database if you use a fast hash or similar. Thus, password hashing is + deliberately designed to be computationally expensive - tens of thousands of rounds of hashing is one simple way to do + it - and is usually tuned to take somewhere between about 50ms and 250ms per user, on security-conscious sites. As + computers get faster, you have to make the password hash even more expensive, too, which isn't a significant concern + with JWT signatures. This means that, with basic auth, you have to either add a ton of CPU time spent doing extremely + expensive password hashing, or you have to run a very high risk that, if your password database is leaked, people will + be able to brute-force the hashes. With JWTs that's not nearly as big a concern (with HMAC-signed JWTs, it's basically + not a concern at all). +* Relatedly, JWTs can be verified without a database lookup at all. Passwords, including ones sent in basic auth, can't. + You can cache them, but that takes server-side resources and breaks down when you have a cluster of servers behind a + load balancer. Thus, JWTs are much better for scaling to large numbers of users, where DB hits are relatively + expensive and need to be minimized. +* Basic auth isn't really compatible with multi-factor authentication (sometimes called two-step authentication or + similar). MFA is usually implemented with ephemeral values - one-time codes generated in an app or sent via SMS, + challenge nonces sent to hardware tokens or push notifications, etc. - and even if there was a practical way to + include the resulting token in the basic auth string (there isn't, for browsers), it would become invalid almost + immediately. That's awkward if you want a session to last more than a few minutes before the user has to log in again! + With a JWT (or any other session token), you can verify the user's credentials (including MFA) once, and then give + them a token good for as long as you want (with JWTs in particular, it's often two tokens: the JWT itself which has a + short lifetime, and a refresh token that lasts longer but takes a DB lookup to verify). +* A bit of a technical curiosity rather than a deliberate feature of JWT design, but: HTTP Basic Auth is automatically + sent by the browser for any request to the relevant domain... even if the request comes from a different domain. This + means that there's a risk of CSRF (Cross-Site Request Forgery), where if site X thinks you might be signed in to site + Y, it can make your browser send requests to Y *and Y will think you, the user, did it*. This could be used for + anything from posting messages on a forum to transferring money between accounts, and it would happen without your + awareness. JWTs, by comparison, can be put in lots of different places. Cookies are common, and *used to be* + automatically sent even on requests from other domains (just like Basic Auth), but these days there's the `SameSite` + cookie flag that can be used to control this behavior. JWTs are also often sent in other HTTP request headers ( + often `Authorization` - the same one used for basic auth - but sometimes custom ones), and in those cases the + requesting client must explicitly specify to send the JWT. Which means the requesting client has to know the full + contents of the JWT, because your browser won't just automatically attach it to every relevant request. +* Logging out a user who uses a JWT is simple, just delete it from their client (this doesn't invalidate the JWT on the + server - that's hard to do, and usually you just rely on the JWT having a short lifetime and expiring soon - but it + works fine for Alice getting up from the computer before Bob sits down to use it). It turns out that there's no + standardized way to make a browser forget basic auth credentials. At least one browser (Internet Explorer) has a + special API for this, but generally speaking, if you want to log out a client that uses basic auth, the only option is + to tell the browser that their authorization is invalid (which generally makes the browser stop sending it). That's... + messy. Obviously for non-browser clients this is not an issue. +* Relatedly, logging in with basic auth isn't really under the control of the client application (implemented in + HTML/CSS/JS). The web browser chooses what to show (usually a pretty ugly modal credentials prompt). Other forms of + authentication / authorization token allow the web application to create their own login pages, which might include + features like a "forgot password?" link or SSO login options, while a basic auth site just presents the user with a + blank page (the content won't be loaded - or even requested - until the user signs in!) and an ugly pop-up modal box. + diff --git a/api/src/configs.rs b/api/src/configs.rs index 126fec5..bfa0350 100644 --- a/api/src/configs.rs +++ b/api/src/configs.rs @@ -1,69 +1,69 @@ -use database::ConfigDatabase; -use jwt::ConfigJWT; -use modules::utility::env_utils::get_env_var; -use openapi::ConfigOpenApi; - -use crate::constants; - -mod database; -mod jwt; -mod openapi; - -#[derive(Debug, Clone)] -pub struct Config { - pub host: String, - pub port: String, - pub fqdn: String, - pub frontend: String, - pub jwt: ConfigJWT, - pub open_api: ConfigOpenApi, - pub database: ConfigDatabase, -} - -impl Default for Config { - fn default() -> Self { - Config::new() - } -} - -impl Config { - pub fn new() -> Self { - let default_host: Option<&str> = Some(constants::CYB_HOST); - let default_port: Option<&str> = Some(constants::CYB_PORT); - let default_fqdn: Option<&str> = Some(constants::CYB_FQDN); - let default_frontend: Option<&str> = Some(constants::CYB_FRONTEND); - - let host: String = get_env_var("CYB_HOST", default_host); - let port: String = get_env_var("CYB_PORT", default_port); - let fqdn: String = get_env_var("CYB_FQDN", default_fqdn); - let frontend: String = get_env_var("CYB_FRONTEND", default_frontend); - - let jwt: ConfigJWT = ConfigJWT::new(); - let open_api: ConfigOpenApi = ConfigOpenApi::new(); - let database: ConfigDatabase = ConfigDatabase::new(); - - Config { - host, - port, - fqdn, - frontend, - jwt, - open_api, - database, - } - } - - pub fn url_api(&self) -> String { - let host: String = self.host.clone(); - let port: String = self.port.clone(); - format!("{host}:{port}") - } - - pub fn url_database(&self) -> String { - self.database.url() - } - - pub fn jwt_secret_bytes(&self) -> &[u8] { - self.jwt.secret_bytes() - } -} +use database::ConfigDatabase; +use jwt::ConfigJWT; +use modules::utility::env_utils::get_env_var; +use openapi::ConfigOpenApi; + +use crate::constants; + +mod database; +mod jwt; +mod openapi; + +#[derive(Debug, Clone)] +pub struct Config { + pub host: String, + pub port: String, + pub fqdn: String, + pub frontend: String, + pub jwt: ConfigJWT, + pub open_api: ConfigOpenApi, + pub database: ConfigDatabase, +} + +impl Default for Config { + fn default() -> Self { + Config::new() + } +} + +impl Config { + pub fn new() -> Self { + let default_host: Option<&str> = Some(constants::CYB_HOST); + let default_port: Option<&str> = Some(constants::CYB_PORT); + let default_fqdn: Option<&str> = Some(constants::CYB_FQDN); + let default_frontend: Option<&str> = Some(constants::CYB_FRONTEND); + + let host: String = get_env_var("CYB_HOST", default_host); + let port: String = get_env_var("CYB_PORT", default_port); + let fqdn: String = get_env_var("CYB_FQDN", default_fqdn); + let frontend: String = get_env_var("CYB_FRONTEND", default_frontend); + + let jwt: ConfigJWT = ConfigJWT::new(); + let open_api: ConfigOpenApi = ConfigOpenApi::new(); + let database: ConfigDatabase = ConfigDatabase::new(); + + Config { + host, + port, + fqdn, + frontend, + jwt, + open_api, + database, + } + } + + pub fn url_api(&self) -> String { + let host: String = self.host.clone(); + let port: String = self.port.clone(); + format!("{host}:{port}") + } + + pub fn url_database(&self) -> String { + self.database.url() + } + + pub fn jwt_secret_bytes(&self) -> &[u8] { + self.jwt.secret_bytes() + } +} diff --git a/api/src/configs/database.rs b/api/src/configs/database.rs index d7bd9fd..d3cd33c 100644 --- a/api/src/configs/database.rs +++ b/api/src/configs/database.rs @@ -1,57 +1,57 @@ -use modules::utility::env_utils::get_env_var; - -use crate::constants; - -#[derive(Debug, Clone)] -pub struct ConfigDatabase { - pub kind: String, - pub host: String, - pub port: String, - pub name: String, - pub user: String, - pub pass: String, -} - -impl Default for ConfigDatabase { - fn default() -> Self { - ConfigDatabase::new() - } -} - -impl ConfigDatabase { - pub fn new() -> Self { - let default_kind: Option<&str> = Some(constants::CYB_DB_KIND); - let default_host: Option<&str> = Some(constants::CYB_DB_HOST); - let default_port: Option<&str> = Some(constants::CYB_DB_PORT); - let default_name: Option<&str> = Some(constants::CYB_DB_NAME); - let default_user: Option<&str> = Some(constants::CYB_DB_USER); - let default_pass: Option<&str> = Some(constants::CYB_DB_PASS); - - let kind: String = get_env_var("CYB_DB_KIND", default_kind); - let host: String = get_env_var("CYB_DB_HOST", default_host); - let port: String = get_env_var("CYB_DB_PORT", default_port); - let name: String = get_env_var("CYB_DB_NAME", default_name); - let user: String = get_env_var("CYB_DB_USER", default_user); - let pass: String = get_env_var("CYB_DB_PASS", default_pass); - - ConfigDatabase { - kind, - host, - port, - name, - user, - pass, - } - } - - pub fn url(&self) -> String { - let kind: String = self.kind.clone(); - let host: String = self.host.clone(); - let port: String = self.port.clone(); - let name: String = self.name.clone(); - let user: String = self.user.clone(); - let pass: String = self.pass.clone(); - - format!("{kind}://{user}:{pass}@{host}:{port}/{name}") - } -} +use modules::utility::env_utils::get_env_var; + +use crate::constants; + +#[derive(Debug, Clone)] +pub struct ConfigDatabase { + pub kind: String, + pub host: String, + pub port: String, + pub name: String, + pub user: String, + pub pass: String, +} + +impl Default for ConfigDatabase { + fn default() -> Self { + ConfigDatabase::new() + } +} + +impl ConfigDatabase { + pub fn new() -> Self { + let default_kind: Option<&str> = Some(constants::CYB_DB_KIND); + let default_host: Option<&str> = Some(constants::CYB_DB_HOST); + let default_port: Option<&str> = Some(constants::CYB_DB_PORT); + let default_name: Option<&str> = Some(constants::CYB_DB_NAME); + let default_user: Option<&str> = Some(constants::CYB_DB_USER); + let default_pass: Option<&str> = Some(constants::CYB_DB_PASS); + + let kind: String = get_env_var("CYB_DB_KIND", default_kind); + let host: String = get_env_var("CYB_DB_HOST", default_host); + let port: String = get_env_var("CYB_DB_PORT", default_port); + let name: String = get_env_var("CYB_DB_NAME", default_name); + let user: String = get_env_var("CYB_DB_USER", default_user); + let pass: String = get_env_var("CYB_DB_PASS", default_pass); + + ConfigDatabase { + kind, + host, + port, + name, + user, + pass, + } + } + + pub fn url(&self) -> String { + let kind: String = self.kind.clone(); + let host: String = self.host.clone(); + let port: String = self.port.clone(); + let name: String = self.name.clone(); + let user: String = self.user.clone(); + let pass: String = self.pass.clone(); + + format!("{kind}://{user}:{pass}@{host}:{port}/{name}") + } +} diff --git a/api/src/configs/jwt.rs b/api/src/configs/jwt.rs index 6ea646b..75a635c 100644 --- a/api/src/configs/jwt.rs +++ b/api/src/configs/jwt.rs @@ -1,38 +1,38 @@ -use modules::utility::env_utils::get_env_var; - -use crate::constants; - -#[derive(Debug, Clone)] -pub struct ConfigJWT { - pub secret: String, - pub max_age: i64, - pub exp_time: String, -} - -impl Default for ConfigJWT { - fn default() -> Self { - ConfigJWT::new() - } -} - -impl ConfigJWT { - pub fn new() -> Self { - let default_secret: Option<&str> = Some(constants::CYB_JWT_SECRET); - let default_max_age: Option<&str> = Some(constants::CYB_JWT_MAX_AGE); - let default_exp_time: Option<&str> = Some(constants::CYB_JWT_EXP_TIME); - - let secret: String = get_env_var("CYB_JWT_SECRET", default_secret); - let max_age: String = get_env_var("CYB_JWT_MAX_AGE", default_max_age); - let exp_time: String = get_env_var("CYB_JWT_EXP_TIME", default_exp_time); - - ConfigJWT { - secret, - max_age: max_age.parse::().unwrap_or(constants::CYB_JWT_MAX_AGE_INT), - exp_time, - } - } - - pub fn secret_bytes(&self) -> &[u8] { - self.secret.as_bytes() - } -} +use modules::utility::env_utils::get_env_var; + +use crate::constants; + +#[derive(Debug, Clone)] +pub struct ConfigJWT { + pub secret: String, + pub max_age: i64, + pub exp_time: String, +} + +impl Default for ConfigJWT { + fn default() -> Self { + ConfigJWT::new() + } +} + +impl ConfigJWT { + pub fn new() -> Self { + let default_secret: Option<&str> = Some(constants::CYB_JWT_SECRET); + let default_max_age: Option<&str> = Some(constants::CYB_JWT_MAX_AGE); + let default_exp_time: Option<&str> = Some(constants::CYB_JWT_EXP_TIME); + + let secret: String = get_env_var("CYB_JWT_SECRET", default_secret); + let max_age: String = get_env_var("CYB_JWT_MAX_AGE", default_max_age); + let exp_time: String = get_env_var("CYB_JWT_EXP_TIME", default_exp_time); + + ConfigJWT { + secret, + max_age: max_age.parse::().unwrap_or(constants::CYB_JWT_MAX_AGE_INT), + exp_time, + } + } + + pub fn secret_bytes(&self) -> &[u8] { + self.secret.as_bytes() + } +} diff --git a/api/src/configs/openapi.rs b/api/src/configs/openapi.rs index 46f803a..dd7ec08 100644 --- a/api/src/configs/openapi.rs +++ b/api/src/configs/openapi.rs @@ -1,69 +1,69 @@ -use modules::utility::env_utils::get_env_var; - -use crate::constants; - -#[derive(Debug, Clone)] -pub struct ConfigOpenApi { - pub enabled: bool, - pub url_path: String, - pub swagger: ConfigOpenApiUi, - pub scalar: ConfigOpenApiUi, - pub rapi_doc: ConfigOpenApiUi, - pub re_doc: ConfigOpenApiUi, -} - -#[derive(Debug, Clone)] -pub struct ConfigOpenApiUi { - pub enabled: bool, - pub url_path: String, -} - -impl Default for ConfigOpenApi { - fn default() -> Self { - ConfigOpenApi::new() - } -} - -impl ConfigOpenApi { - pub fn new() -> Self { - let default_oapi: Option<&str> = Some(constants::CYB_OAPI); - let default_oapi_path: Option<&str> = Some(constants::CYB_OAPI_PATH); - let default_oapi_swagger: Option<&str> = Some(constants::CYB_OAPI_SWAGGER); - let default_oapi_swagger_path: Option<&str> = Some(constants::CYB_OAPI_SWAGGER_PATH); - let default_oapi_scalar: Option<&str> = Some(constants::CYB_OAPI_SCALAR); - let default_oapi_scalar_path: Option<&str> = Some(constants::CYB_OAPI_SCALAR_PATH); - let default_oapi_rapidoc: Option<&str> = Some(constants::CYB_OAPI_RAPIDOC); - let default_oapi_rapidoc_path: Option<&str> = Some(constants::CYB_OAPI_RAPIDOC_PATH); - let default_oapi_redoc: Option<&str> = Some(constants::CYB_OAPI_REDOC); - let default_oapi_redoc_path: Option<&str> = Some(constants::CYB_OAPI_REDOC_PATH); - - let enabled: bool = get_env_var("CYB_OAPI", default_oapi) == "true"; - let url_path: String = get_env_var("CYB_OAPI_PATH", default_oapi_path); - - let swagger: ConfigOpenApiUi = ConfigOpenApiUi { - enabled: get_env_var("CYB_OAPI_SWAGGER", default_oapi_swagger) == "true", - url_path: get_env_var("CYB_OAPI_SWAGGER_PATH", default_oapi_swagger_path), - }; - let scalar: ConfigOpenApiUi = ConfigOpenApiUi { - enabled: get_env_var("CYB_OAPI_SCALAR", default_oapi_scalar) == "true", - url_path: get_env_var("CYB_OAPI_SCALAR_PATH", default_oapi_scalar_path), - }; - let rapi_doc: ConfigOpenApiUi = ConfigOpenApiUi { - enabled: get_env_var("CYB_OAPI_RAPIDOC", default_oapi_rapidoc) == "true", - url_path: get_env_var("CYB_OAPI_RAPIDOC_PATH", default_oapi_rapidoc_path), - }; - let re_doc: ConfigOpenApiUi = ConfigOpenApiUi { - enabled: get_env_var("CYB_OAPI_REDOC", default_oapi_redoc) == "true", - url_path: get_env_var("CYB_OAPI_REDOC_PATH", default_oapi_redoc_path), - }; - - ConfigOpenApi { - enabled, - url_path, - swagger, - scalar, - rapi_doc, - re_doc, - } - } -} +use modules::utility::env_utils::get_env_var; + +use crate::constants; + +#[derive(Debug, Clone)] +pub struct ConfigOpenApi { + pub enabled: bool, + pub url_path: String, + pub swagger: ConfigOpenApiUi, + pub scalar: ConfigOpenApiUi, + pub rapi_doc: ConfigOpenApiUi, + pub re_doc: ConfigOpenApiUi, +} + +#[derive(Debug, Clone)] +pub struct ConfigOpenApiUi { + pub enabled: bool, + pub url_path: String, +} + +impl Default for ConfigOpenApi { + fn default() -> Self { + ConfigOpenApi::new() + } +} + +impl ConfigOpenApi { + pub fn new() -> Self { + let default_oapi: Option<&str> = Some(constants::CYB_OAPI); + let default_oapi_path: Option<&str> = Some(constants::CYB_OAPI_PATH); + let default_oapi_swagger: Option<&str> = Some(constants::CYB_OAPI_SWAGGER); + let default_oapi_swagger_path: Option<&str> = Some(constants::CYB_OAPI_SWAGGER_PATH); + let default_oapi_scalar: Option<&str> = Some(constants::CYB_OAPI_SCALAR); + let default_oapi_scalar_path: Option<&str> = Some(constants::CYB_OAPI_SCALAR_PATH); + let default_oapi_rapidoc: Option<&str> = Some(constants::CYB_OAPI_RAPIDOC); + let default_oapi_rapidoc_path: Option<&str> = Some(constants::CYB_OAPI_RAPIDOC_PATH); + let default_oapi_redoc: Option<&str> = Some(constants::CYB_OAPI_REDOC); + let default_oapi_redoc_path: Option<&str> = Some(constants::CYB_OAPI_REDOC_PATH); + + let enabled: bool = get_env_var("CYB_OAPI", default_oapi) == "true"; + let url_path: String = get_env_var("CYB_OAPI_PATH", default_oapi_path); + + let swagger: ConfigOpenApiUi = ConfigOpenApiUi { + enabled: get_env_var("CYB_OAPI_SWAGGER", default_oapi_swagger) == "true", + url_path: get_env_var("CYB_OAPI_SWAGGER_PATH", default_oapi_swagger_path), + }; + let scalar: ConfigOpenApiUi = ConfigOpenApiUi { + enabled: get_env_var("CYB_OAPI_SCALAR", default_oapi_scalar) == "true", + url_path: get_env_var("CYB_OAPI_SCALAR_PATH", default_oapi_scalar_path), + }; + let rapi_doc: ConfigOpenApiUi = ConfigOpenApiUi { + enabled: get_env_var("CYB_OAPI_RAPIDOC", default_oapi_rapidoc) == "true", + url_path: get_env_var("CYB_OAPI_RAPIDOC_PATH", default_oapi_rapidoc_path), + }; + let re_doc: ConfigOpenApiUi = ConfigOpenApiUi { + enabled: get_env_var("CYB_OAPI_REDOC", default_oapi_redoc) == "true", + url_path: get_env_var("CYB_OAPI_REDOC_PATH", default_oapi_redoc_path), + }; + + ConfigOpenApi { + enabled, + url_path, + swagger, + scalar, + rapi_doc, + re_doc, + } + } +} diff --git a/api/src/constants.rs b/api/src/constants.rs index 82723b6..7148081 100644 --- a/api/src/constants.rs +++ b/api/src/constants.rs @@ -1,27 +1,27 @@ -pub const CYB_HOST: &str = "127.0.0.1"; -pub const CYB_PORT: &str = "8000"; -pub const CYB_FQDN: &str = "cyb.app"; -pub const CYB_FRONTEND: &str = ""; - -pub const CYB_DB_KIND: &str = "postgres"; -pub const CYB_DB_HOST: &str = "127.0.0.1"; -pub const CYB_DB_PORT: &str = "5432"; -pub const CYB_DB_NAME: &str = "cyb"; -pub const CYB_DB_USER: &str = "cyb"; -pub const CYB_DB_PASS: &str = "cyb"; - -pub const CYB_JWT_SECRET: &str = "C59A1381A6D2E26B3FBCB83618444296"; -pub const CYB_JWT_MAX_AGE: &str = "60"; -pub const CYB_JWT_MAX_AGE_INT: i64 = 60; -pub const CYB_JWT_EXP_TIME: &str = "60m"; - -pub const CYB_OAPI: &str = "true"; -pub const CYB_OAPI_PATH: &str = "openapi.json"; -pub const CYB_OAPI_SWAGGER: &str = "true"; -pub const CYB_OAPI_SWAGGER_PATH: &str = "swagger=ui"; -pub const CYB_OAPI_SCALAR: &str = "true"; -pub const CYB_OAPI_SCALAR_PATH: &str = "scalar"; -pub const CYB_OAPI_RAPIDOC: &str = "true"; -pub const CYB_OAPI_RAPIDOC_PATH: &str = "rapidoc"; -pub const CYB_OAPI_REDOC: &str = "true"; -pub const CYB_OAPI_REDOC_PATH: &str = "redoc"; +pub const CYB_HOST: &str = "127.0.0.1"; +pub const CYB_PORT: &str = "8000"; +pub const CYB_FQDN: &str = "cyb.app"; +pub const CYB_FRONTEND: &str = ""; + +pub const CYB_DB_KIND: &str = "postgres"; +pub const CYB_DB_HOST: &str = "127.0.0.1"; +pub const CYB_DB_PORT: &str = "5432"; +pub const CYB_DB_NAME: &str = "cyb"; +pub const CYB_DB_USER: &str = "cyb"; +pub const CYB_DB_PASS: &str = "cyb"; + +pub const CYB_JWT_SECRET: &str = "C59A1381A6D2E26B3FBCB83618444296"; +pub const CYB_JWT_MAX_AGE: &str = "60"; +pub const CYB_JWT_MAX_AGE_INT: i64 = 60; +pub const CYB_JWT_EXP_TIME: &str = "60m"; + +pub const CYB_OAPI: &str = "true"; +pub const CYB_OAPI_PATH: &str = "openapi.json"; +pub const CYB_OAPI_SWAGGER: &str = "true"; +pub const CYB_OAPI_SWAGGER_PATH: &str = "swagger=ui"; +pub const CYB_OAPI_SCALAR: &str = "true"; +pub const CYB_OAPI_SCALAR_PATH: &str = "scalar"; +pub const CYB_OAPI_RAPIDOC: &str = "true"; +pub const CYB_OAPI_RAPIDOC_PATH: &str = "rapidoc"; +pub const CYB_OAPI_REDOC: &str = "true"; +pub const CYB_OAPI_REDOC_PATH: &str = "redoc"; diff --git a/api/src/databases.rs b/api/src/databases.rs index b79333f..1d98986 100644 --- a/api/src/databases.rs +++ b/api/src/databases.rs @@ -1,18 +1,18 @@ -use database::sea_orm::{ConnectOptions, Database, DatabaseConnection}; -use migration::{Migrator, MigratorTrait}; - -use crate::configs::Config; - -pub async fn setup_database(configs: &Config) -> DatabaseConnection { - // database connection options. - let mut options: ConnectOptions = ConnectOptions::new(configs.url_database()); - options.sqlx_logging(false); - - // connect to database. - let connection: DatabaseConnection = Database::connect(options).await.unwrap(); - - // apply all pending migrations. - Migrator::up(&connection, None).await.unwrap(); - - connection -} +use database::sea_orm::{ConnectOptions, Database, DatabaseConnection}; +use migration::{Migrator, MigratorTrait}; + +use crate::configs::Config; + +pub async fn setup_database(configs: &Config) -> DatabaseConnection { + // database connection options. + let mut options: ConnectOptions = ConnectOptions::new(configs.url_database()); + options.sqlx_logging(false); + + // connect to database. + let connection: DatabaseConnection = Database::connect(options).await.unwrap(); + + // apply all pending migrations. + Migrator::up(&connection, None).await.unwrap(); + + connection +} diff --git a/api/src/middlewares.rs b/api/src/middlewares.rs index e71cb49..c314752 100644 --- a/api/src/middlewares.rs +++ b/api/src/middlewares.rs @@ -1,48 +1,48 @@ -use salvo::compression::Compression; -use salvo::cors::{Cors, CorsHandler}; -use salvo::http::Method; -use salvo::jwt_auth::{ConstDecoder, HeaderFinder, QueryFinder}; -use salvo::prelude::*; - -use crate::schemas::{AppState, JwtClaims}; - -pub fn setup(app_state: &AppState) -> Router { - // create necessary middlewares. - let logger: Logger = Logger::new(); - - let auth_handler: JwtAuth = JwtAuth::new(ConstDecoder::from_secret( - app_state.configs.jwt_secret_bytes(), - )) - .finders(vec![ - Box::new(HeaderFinder::new()), - Box::new(QueryFinder::new("jwt_token")), - ]) - .force_passed(true); - - let cache: CachingHeaders = CachingHeaders::new(); - - let compression: Compression = Compression::new() - .disable_all() - .enable_brotli(CompressionLevel::Minsize) - .force_priority(true); - - let cors_handler: CorsHandler = Cors::new() - .allow_origin(&app_state.configs.url_api()) - .allow_methods(vec![ - Method::GET, - Method::POST, - Method::HEAD, - Method::OPTIONS, - ]) - .into_handler(); - - // create a router with all the middlewares. - Router::new() - .hoop(affix::inject(app_state.clone())) - .hoop(logger) - .hoop(auth_handler) - .hoop(cache) - .hoop(compression) - .hoop(cors_handler) - .options(handler::empty()) -} +use salvo::compression::Compression; +use salvo::cors::{Cors, CorsHandler}; +use salvo::http::Method; +use salvo::jwt_auth::{ConstDecoder, HeaderFinder, QueryFinder}; +use salvo::prelude::*; + +use crate::schemas::{AppState, JwtClaims}; + +pub fn setup(app_state: &AppState) -> Router { + // create necessary middlewares. + let logger: Logger = Logger::new(); + + let auth_handler: JwtAuth = JwtAuth::new(ConstDecoder::from_secret( + app_state.configs.jwt_secret_bytes(), + )) + .finders(vec![ + Box::new(HeaderFinder::new()), + Box::new(QueryFinder::new("jwt_token")), + ]) + .force_passed(true); + + let cache: CachingHeaders = CachingHeaders::new(); + + let compression: Compression = Compression::new() + .disable_all() + .enable_brotli(CompressionLevel::Minsize) + .force_priority(true); + + let cors_handler: CorsHandler = Cors::new() + .allow_origin(&app_state.configs.url_api()) + .allow_methods(vec![ + Method::GET, + Method::POST, + Method::HEAD, + Method::OPTIONS, + ]) + .into_handler(); + + // create a router with all the middlewares. + Router::new() + .hoop(affix::inject(app_state.clone())) + .hoop(logger) + .hoop(auth_handler) + .hoop(cache) + .hoop(compression) + .hoop(cors_handler) + .options(handler::empty()) +} diff --git a/api/src/routers.rs b/api/src/routers.rs index 5395fed..e93bfa4 100644 --- a/api/src/routers.rs +++ b/api/src/routers.rs @@ -1,70 +1,70 @@ -use salvo::Router; - -use crate::routers::docs::{ - setup_open_api, setup_rapidoc, setup_redoc, setup_scalar, setup_swagger, -}; -use crate::schemas::AppState; - -mod docs; -mod healthcheck; -mod statics; -mod v1; - -pub fn setup(app_state: &AppState, middlewares: Router) -> Router { - // merge all routers into one router. - let mut router: Router = Router::new().path("api").push(healthcheck::setup()); - - router = middlewares.push(router); - - if app_state.configs.open_api.enabled { - // generate openapi.json. - let open_api_path: String = app_state.configs.open_api.url_path.clone(); - router = setup_open_api( - router, - &open_api_path, - "Connect Your Books OpenAPI Document.", - "v0.0.1", - ); - - if app_state.configs.open_api.swagger.enabled { - // enable swagger ui. - router = setup_swagger( - router, - app_state.configs.open_api.swagger.url_path.clone(), - open_api_path.clone(), - ); - } - - if app_state.configs.open_api.scalar.enabled { - // enable scalar. - router = setup_scalar( - router, - app_state.configs.open_api.scalar.url_path.clone(), - open_api_path.clone(), - ); - } - - if app_state.configs.open_api.rapi_doc.enabled { - // enable rapidoc. - router = setup_rapidoc( - router, - app_state.configs.open_api.rapi_doc.url_path.clone(), - open_api_path.clone(), - ); - } - - if app_state.configs.open_api.re_doc.enabled { - // enable redoc. - router = setup_redoc( - router, - app_state.configs.open_api.re_doc.url_path.clone(), - open_api_path.clone(), - ); - } - } - - // push static assets into routers. - router = router.push(statics::setup_frontend(app_state.configs.frontend.clone())); - - router -} +use salvo::Router; + +use crate::routers::docs::{ + setup_open_api, setup_rapidoc, setup_redoc, setup_scalar, setup_swagger, +}; +use crate::schemas::AppState; + +mod docs; +mod healthcheck; +mod statics; +mod v1; + +pub fn setup(app_state: &AppState, middlewares: Router) -> Router { + // merge all routers into one router. + let mut router: Router = Router::new().path("api").push(healthcheck::setup()); + + router = middlewares.push(router); + + if app_state.configs.open_api.enabled { + // generate openapi.json. + let open_api_path: String = app_state.configs.open_api.url_path.clone(); + router = setup_open_api( + router, + &open_api_path, + "Connect Your Books OpenAPI Document.", + "v0.0.1", + ); + + if app_state.configs.open_api.swagger.enabled { + // enable swagger ui. + router = setup_swagger( + router, + app_state.configs.open_api.swagger.url_path.clone(), + open_api_path.clone(), + ); + } + + if app_state.configs.open_api.scalar.enabled { + // enable scalar. + router = setup_scalar( + router, + app_state.configs.open_api.scalar.url_path.clone(), + open_api_path.clone(), + ); + } + + if app_state.configs.open_api.rapi_doc.enabled { + // enable rapidoc. + router = setup_rapidoc( + router, + app_state.configs.open_api.rapi_doc.url_path.clone(), + open_api_path.clone(), + ); + } + + if app_state.configs.open_api.re_doc.enabled { + // enable redoc. + router = setup_redoc( + router, + app_state.configs.open_api.re_doc.url_path.clone(), + open_api_path.clone(), + ); + } + } + + // push static assets into routers. + router = router.push(statics::setup_frontend(app_state.configs.frontend.clone())); + + router +} diff --git a/api/src/routers/docs.rs b/api/src/routers/docs.rs index 6fc4d50..cfd8801 100644 --- a/api/src/routers/docs.rs +++ b/api/src/routers/docs.rs @@ -1,41 +1,41 @@ -use salvo::prelude::*; - -pub fn setup_open_api(router: Router, path: &str, title: &str, version: &str) -> Router { - // create api documentation from routers. - let docs: OpenApi = OpenApi::new(title, version).merge_router(&router); - - // push to router. - router.unshift(docs.into_router(path)) -} - -pub fn setup_swagger(router: Router, path: String, open_api_path: String) -> Router { - // create swagger ui from json. - let swagger: SwaggerUi = SwaggerUi::new(open_api_path); - - // push to router. - router.unshift(swagger.into_router(path)) -} - -pub fn setup_scalar(router: Router, path: String, open_api_path: String) -> Router { - // create scalar from json. - let scalar: Scalar = Scalar::new(open_api_path); - - // push to router. - router.unshift(scalar.into_router(path)) -} - -pub fn setup_rapidoc(router: Router, path: String, open_api_path: String) -> Router { - // create rapidoc from json. - let rapidoc: RapiDoc = RapiDoc::new(open_api_path); - - // push to router. - router.unshift(rapidoc.into_router(path)) -} - -pub fn setup_redoc(router: Router, path: String, open_api_path: String) -> Router { - // create redoc from json. - let redoc: ReDoc = ReDoc::new(open_api_path); - - // push to router. - router.unshift(redoc.into_router(path)) -} +use salvo::prelude::*; + +pub fn setup_open_api(router: Router, path: &str, title: &str, version: &str) -> Router { + // create api documentation from routers. + let docs: OpenApi = OpenApi::new(title, version).merge_router(&router); + + // push to router. + router.unshift(docs.into_router(path)) +} + +pub fn setup_swagger(router: Router, path: String, open_api_path: String) -> Router { + // create swagger ui from json. + let swagger: SwaggerUi = SwaggerUi::new(open_api_path); + + // push to router. + router.unshift(swagger.into_router(path)) +} + +pub fn setup_scalar(router: Router, path: String, open_api_path: String) -> Router { + // create scalar from json. + let scalar: Scalar = Scalar::new(open_api_path); + + // push to router. + router.unshift(scalar.into_router(path)) +} + +pub fn setup_rapidoc(router: Router, path: String, open_api_path: String) -> Router { + // create rapidoc from json. + let rapidoc: RapiDoc = RapiDoc::new(open_api_path); + + // push to router. + router.unshift(rapidoc.into_router(path)) +} + +pub fn setup_redoc(router: Router, path: String, open_api_path: String) -> Router { + // create redoc from json. + let redoc: ReDoc = ReDoc::new(open_api_path); + + // push to router. + router.unshift(redoc.into_router(path)) +} diff --git a/api/src/routers/healthcheck.rs b/api/src/routers/healthcheck.rs index fe1c104..d110fb9 100644 --- a/api/src/routers/healthcheck.rs +++ b/api/src/routers/healthcheck.rs @@ -1,12 +1,12 @@ -use salvo::http::{StatusCode, StatusError}; -use salvo::prelude::*; -use salvo::Router; - -pub fn setup() -> Router { - Router::new().path("healthcheck").get(healthcheck) -} - -#[endpoint(status_codes(200, 500))] -async fn healthcheck() -> Result { - Ok(StatusCode::OK) -} +use salvo::http::{StatusCode, StatusError}; +use salvo::prelude::*; +use salvo::Router; + +pub fn setup() -> Router { + Router::new().path("healthcheck").get(healthcheck) +} + +#[endpoint(status_codes(200, 500))] +async fn healthcheck() -> Result { + Ok(StatusCode::OK) +} diff --git a/api/src/routers/statics.rs b/api/src/routers/statics.rs index 2977f69..986be6d 100644 --- a/api/src/routers/statics.rs +++ b/api/src/routers/statics.rs @@ -1,8 +1,8 @@ -use salvo::prelude::*; -use salvo::serve_static::StaticDir; - -pub fn setup_frontend(path: String) -> Router { - let frontend: StaticDir = StaticDir::new([path]).defaults("index.html").auto_list(true); - - Router::new().path("<**rest_path>").get(frontend) -} +use salvo::prelude::*; +use salvo::serve_static::StaticDir; + +pub fn setup_frontend(path: String) -> Router { + let frontend: StaticDir = StaticDir::new([path]).defaults("index.html").auto_list(true); + + Router::new().path("<**rest_path>").get(frontend) +} diff --git a/api/src/schemas.rs b/api/src/schemas.rs index b881824..6bdf6a5 100644 --- a/api/src/schemas.rs +++ b/api/src/schemas.rs @@ -1,37 +1,37 @@ -use serde::{Deserialize, Serialize}; - -use database::sea_orm::DatabaseConnection; - -use crate::configs::Config; - -#[derive(Debug, Clone)] -pub struct AppState { - pub db_connection: DatabaseConnection, - pub configs: Config, -} - -#[derive(Debug, Deserialize)] -pub struct SignUpUserSchema { - pub name_first: String, - pub name_last: String, - pub name_user: String, - pub password: String, - pub email: Option, -} - -#[derive(Debug, Deserialize)] -pub struct SignInUserSchema { - pub name_user: String, - pub password: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct JwtClaims { - // (subject): Subject of the JWT (the user) - pub sub: String, - // (issued at time): Time at which the JWT was issued; - // can be used to determine age of the JWT. - pub iat: i64, - // (expiration time): Time after which the JWT expires - pub exp: i64, -} +use serde::{Deserialize, Serialize}; + +use database::sea_orm::DatabaseConnection; + +use crate::configs::Config; + +#[derive(Debug, Clone)] +pub struct AppState { + pub db_connection: DatabaseConnection, + pub configs: Config, +} + +#[derive(Debug, Deserialize)] +pub struct SignUpUserSchema { + pub name_first: String, + pub name_last: String, + pub name_user: String, + pub password: String, + pub email: Option, +} + +#[derive(Debug, Deserialize)] +pub struct SignInUserSchema { + pub name_user: String, + pub password: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JwtClaims { + // (subject): Subject of the JWT (the user) + pub sub: String, + // (issued at time): Time at which the JWT was issued; + // can be used to determine age of the JWT. + pub iat: i64, + // (expiration time): Time after which the JWT expires + pub exp: i64, +} diff --git a/database/docs/setup.md b/database/docs/setup.md index 08a461e..887d24a 100644 --- a/database/docs/setup.md +++ b/database/docs/setup.md @@ -1,17 +1,17 @@ -You have to install SeaORM CLI for migration tasks. -```shell -cargo install sea-orm-cli -``` - -```shell -sea-orm-cli migrate generate users -``` - -```shell -sea-orm-cli migrate -u postgres://cyb:cyb@127.0.0.1:5432/cyb -``` - -```shell -cd entity/src -sea-orm-cli generate entity -u postgres://cyb:cyb@127.0.0.1:5432/cyb -``` +You have to install SeaORM CLI for migration tasks. +```shell +cargo install sea-orm-cli +``` + +```shell +sea-orm-cli migrate generate users +``` + +```shell +sea-orm-cli migrate -u postgres://cyb:cyb@127.0.0.1:5432/cyb +``` + +```shell +cd entity/src +sea-orm-cli generate entity -u postgres://cyb:cyb@127.0.0.1:5432/cyb +``` diff --git a/deny.toml b/deny.toml index 9e9a69a..e037b1a 100644 --- a/deny.toml +++ b/deny.toml @@ -1,43 +1,43 @@ -# cargo-deny is really only ever intended to run on the "normal" tier-1 targets -targets = [ - { triple = "x86_64-unknown-linux-gnu" }, - { triple = "aarch64-unknown-linux-gnu" }, - { triple = "x86_64-unknown-linux-musl" }, - { triple = "aarch64-apple-darwin" }, - { triple = "x86_64-apple-darwin" }, - { triple = "x86_64-pc-windows-msvc" }, -] - -[advisories] -vulnerability = "deny" -unmaintained = "deny" -notice = "deny" -unsound = "deny" - -[sources] -unknown-registry = "deny" -unknown-git = "deny" - -[licenses] -unlicensed = "deny" -allow-osi-fsf-free = "both" -copyleft = "allow" -# We want really high confidence when inferring licenses from text -confidence-threshold = 0.93 - -exceptions = [ - { allow = ["OpenSSL"], name = "ring" }, - { allow = ["Unicode-DFS-2016"], name = "unicode-ident" }, -] - -[[licenses.clarify]] -name = "ring" -# SPDX considers OpenSSL to encompass both the OpenSSL and SSLeay licenses -# https://spdx.org/licenses/OpenSSL.html -# ISC - Both BoringSSL and ring use this for their new files -# MIT - "Files in third_party/ have their own licenses, as described therein. The MIT -# license, for third_party/fiat, which, unlike other third_party directories, is -# compiled into non-test libraries, is included below." -# OpenSSL - Obviously -expression = "ISC AND MIT AND OpenSSL" -license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] +# cargo-deny is really only ever intended to run on the "normal" tier-1 targets +targets = [ + { triple = "x86_64-unknown-linux-gnu" }, + { triple = "aarch64-unknown-linux-gnu" }, + { triple = "x86_64-unknown-linux-musl" }, + { triple = "aarch64-apple-darwin" }, + { triple = "x86_64-apple-darwin" }, + { triple = "x86_64-pc-windows-msvc" }, +] + +[advisories] +vulnerability = "deny" +unmaintained = "deny" +notice = "deny" +unsound = "deny" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" + +[licenses] +unlicensed = "deny" +allow-osi-fsf-free = "both" +copyleft = "allow" +# We want really high confidence when inferring licenses from text +confidence-threshold = 0.93 + +exceptions = [ + { allow = ["OpenSSL"], name = "ring" }, + { allow = ["Unicode-DFS-2016"], name = "unicode-ident" }, +] + +[[licenses.clarify]] +name = "ring" +# SPDX considers OpenSSL to encompass both the OpenSSL and SSLeay licenses +# https://spdx.org/licenses/OpenSSL.html +# ISC - Both BoringSSL and ring use this for their new files +# MIT - "Files in third_party/ have their own licenses, as described therein. The MIT +# license, for third_party/fiat, which, unlike other third_party directories, is +# compiled into non-test libraries, is included below." +# OpenSSL - Obviously +expression = "ISC AND MIT AND OpenSSL" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] diff --git a/docker/base.Dockerfile b/docker/base.Dockerfile index 62eb81a..9dc0afc 100644 --- a/docker/base.Dockerfile +++ b/docker/base.Dockerfile @@ -1,40 +1,40 @@ -FROM rust:1.74-bookworm as build - -# create a new empty shell project. -RUN USER=root cargo new --bin cyb -WORKDIR /cyb - -# create project structure directories. -RUN mkdir --parents api database/entity database/migration frontend modules - -# copy over cargo manifests -COPY ./Cargo.lock ./Cargo.lock -COPY ./Cargo.toml ./Cargo.toml -COPY ./api/Cargo.toml ./api/Cargo.toml -COPY ./database/Cargo.toml ./database/Cargo.toml -COPY ./database/entity/Cargo.toml ./database/entity/Cargo.toml -COPY ./database/migration/Cargo.toml ./database/migration/Cargo.toml -COPY ./frontend/Cargo.toml ./frontend/Cargo.toml -COPY ./modules/Cargo.toml ./modules/Cargo.toml - -# copy project source tree. -COPY ./src ./src -COPY ./api/src ./api/src -COPY ./database/src ./database/src -COPY ./database/tests ./database/tests -COPY ./database/entity/src ./database/entity/src -COPY ./database/migration/src ./database/migration/src -COPY ./frontend/src ./frontend/src -COPY ./modules/src ./modules/src - -# build for release -RUN cargo build --release - -# our final base -FROM debian:bookworm-slim - -# copy the build artifact from the build stage -COPY --from=build /cyb/target/release/cyb . - -# set the startup command to run your binary -CMD ["./cyb"] +FROM rust:1.74-bookworm as build + +# create a new empty shell project. +RUN USER=root cargo new --bin cyb +WORKDIR /cyb + +# create project structure directories. +RUN mkdir --parents api database/entity database/migration frontend modules + +# copy over cargo manifests +COPY ./Cargo.lock ./Cargo.lock +COPY ./Cargo.toml ./Cargo.toml +COPY ./api/Cargo.toml ./api/Cargo.toml +COPY ./database/Cargo.toml ./database/Cargo.toml +COPY ./database/entity/Cargo.toml ./database/entity/Cargo.toml +COPY ./database/migration/Cargo.toml ./database/migration/Cargo.toml +COPY ./frontend/Cargo.toml ./frontend/Cargo.toml +COPY ./modules/Cargo.toml ./modules/Cargo.toml + +# copy project source tree. +COPY ./src ./src +COPY ./api/src ./api/src +COPY ./database/src ./database/src +COPY ./database/tests ./database/tests +COPY ./database/entity/src ./database/entity/src +COPY ./database/migration/src ./database/migration/src +COPY ./frontend/src ./frontend/src +COPY ./modules/src ./modules/src + +# build for release +RUN cargo build --release + +# our final base +FROM debian:bookworm-slim + +# copy the build artifact from the build stage +COPY --from=build /cyb/target/release/cyb . + +# set the startup command to run your binary +CMD ["./cyb"] diff --git a/frontend/public/app.webmanifest b/frontend/public/app.webmanifest index 5978f80..2dc4854 100644 --- a/frontend/public/app.webmanifest +++ b/frontend/public/app.webmanifest @@ -1,29 +1,29 @@ -{ - "name": "Connect Your Books", - "short_name": "CYB", - "icons": [ - { - "src": "/assets/icons/192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "/assets/icons/384.png", - "type": "image/png", - "sizes": "384x384" - }, - { - "src": "/assets/icons/512.png", - "type": "image/png", - "sizes": "512x512" - }, - { - "src": "/assets/icons/1024.png", - "type": "image/png", - "sizes": "1024x1024" - } - ], - "start_url": "/", - "display": "standalone", - "id": "23675cdf-f809-49ca-b05b-79a9dbb43b85" +{ + "name": "Connect Your Books", + "short_name": "CYB", + "icons": [ + { + "src": "/assets/icons/192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "/assets/icons/384.png", + "type": "image/png", + "sizes": "384x384" + }, + { + "src": "/assets/icons/512.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "/assets/icons/1024.png", + "type": "image/png", + "sizes": "1024x1024" + } + ], + "start_url": "/", + "display": "standalone", + "id": "23675cdf-f809-49ca-b05b-79a9dbb43b85" } \ No newline at end of file diff --git a/frontend/src/components/mod.rs b/frontend/src/components/mod.rs index 9e9af7a..fa2db9c 100644 --- a/frontend/src/components/mod.rs +++ b/frontend/src/components/mod.rs @@ -1,3 +1,3 @@ -pub mod footer; -pub mod navbar; -pub mod svg; +pub mod footer; +pub mod navbar; +pub mod svg; diff --git a/frontend/src/constants.rs b/frontend/src/constants.rs index a014f4e..abfcef4 100644 --- a/frontend/src/constants.rs +++ b/frontend/src/constants.rs @@ -1,11 +1,11 @@ -// TODO: remove these stubs -pub const DASHBOARD: &str = "dashboard"; -pub const TEAM: &str = "team"; -pub const PROJECTS: &str = "projects"; -pub const CALENDAR: &str = "calendar"; -pub const REPORTS: &str = "reports"; -pub const STORAGE_KEY: &str = "cyb_auth_key"; -pub const API_URL: &str = "https://0.0.0.0:/api"; - -pub const AUTHENTICATION: &str = "authentication"; - +// TODO: remove these stubs +pub const DASHBOARD: &str = "dashboard"; +pub const TEAM: &str = "team"; +pub const PROJECTS: &str = "projects"; +pub const CALENDAR: &str = "calendar"; +pub const REPORTS: &str = "reports"; +pub const STORAGE_KEY: &str = "cyb_auth_key"; +pub const API_URL: &str = "https://0.0.0.0:/api"; + +pub const AUTHENTICATION: &str = "authentication"; + diff --git a/frontend/src/inits.rs b/frontend/src/inits.rs index a9efadb..61c61ac 100644 --- a/frontend/src/inits.rs +++ b/frontend/src/inits.rs @@ -1,93 +1,93 @@ -use gloo_storage::{LocalStorage, Storage}; -use seed::prelude::*; - -use crate::constants; -use crate::models; -use crate::pages; -use crate::updates; -use crate::urls; - -// `init` describes what should happen when your app started. -pub fn init(url: Url, orders: &mut impl Orders) -> models::Model { - orders - .subscribe(updates::Msg::UrlChanged) - .stream(streams::window_event(Ev::Click, |_| { - updates::Msg::HideProfileMenu - })); - - let user: Option = LocalStorage::get(constants::STORAGE_KEY).ok(); - models::Model { - ctx: models::Context { user: user.clone() }, - url: url.to_hash_base_url(), - page: pages::Pages::init(url.clone(), orders, user.as_ref()), - is_dark_mode: false, - navbar: navbar(&url.to_hash_base_url()), - navbar_active_item_id: active_navbar_item_id(url), - navbar_hamburger_menu_visible: false, - profile_menu: profile_menu(), - profile_menu_visible: false, - secret_message: None, - } -} - -fn navbar(base_url: &Url) -> Vec { - vec![ - models::NavigationItem { - id: 0, - name: "Dashboard".to_string(), - href: urls::Urls::new(base_url).dashboard().base().to_string(), - }, - models::NavigationItem { - id: 1, - name: "Team".to_string(), - href: urls::Urls::new(base_url).team().base().to_string(), - }, - models::NavigationItem { - id: 2, - name: "Projects".to_string(), - href: urls::Urls::new(base_url).projects().base().to_string(), - }, - models::NavigationItem { - id: 3, - name: "Calendar".to_string(), - href: urls::Urls::new(base_url).calendar().base().to_string(), - }, - models::NavigationItem { - id: 4, - name: "Reports".to_string(), - href: urls::Urls::new(base_url).reports().base().to_string(), - }, - ] -} - -fn active_navbar_item_id(mut url: Url) -> u8 { - match url.remaining_hash_path_parts().as_slice() { - [] => 10, - [constants::DASHBOARD] => 0, - [constants::TEAM] => 1, - [constants::PROJECTS] => 2, - [constants::CALENDAR] => 3, - [constants::REPORTS] => 4, - _ => 10, - } -} - -fn profile_menu() -> Vec { - vec![ - models::NavigationItem { - id: 0, - name: "Your Profile".to_string(), - href: "#".to_string(), - }, - models::NavigationItem { - id: 1, - name: "Settings".to_string(), - href: "#".to_string(), - }, - models::NavigationItem { - id: 2, - name: "Sign out".to_string(), - href: "#".to_string(), - }, - ] -} +use gloo_storage::{LocalStorage, Storage}; +use seed::prelude::*; + +use crate::constants; +use crate::models; +use crate::pages; +use crate::updates; +use crate::urls; + +// `init` describes what should happen when your app started. +pub fn init(url: Url, orders: &mut impl Orders) -> models::Model { + orders + .subscribe(updates::Msg::UrlChanged) + .stream(streams::window_event(Ev::Click, |_| { + updates::Msg::HideProfileMenu + })); + + let user: Option = LocalStorage::get(constants::STORAGE_KEY).ok(); + models::Model { + ctx: models::Context { user: user.clone() }, + url: url.to_hash_base_url(), + page: pages::Pages::init(url.clone(), orders, user.as_ref()), + is_dark_mode: false, + navbar: navbar(&url.to_hash_base_url()), + navbar_active_item_id: active_navbar_item_id(url), + navbar_hamburger_menu_visible: false, + profile_menu: profile_menu(), + profile_menu_visible: false, + secret_message: None, + } +} + +fn navbar(base_url: &Url) -> Vec { + vec![ + models::NavigationItem { + id: 0, + name: "Dashboard".to_string(), + href: urls::Urls::new(base_url).dashboard().base().to_string(), + }, + models::NavigationItem { + id: 1, + name: "Team".to_string(), + href: urls::Urls::new(base_url).team().base().to_string(), + }, + models::NavigationItem { + id: 2, + name: "Projects".to_string(), + href: urls::Urls::new(base_url).projects().base().to_string(), + }, + models::NavigationItem { + id: 3, + name: "Calendar".to_string(), + href: urls::Urls::new(base_url).calendar().base().to_string(), + }, + models::NavigationItem { + id: 4, + name: "Reports".to_string(), + href: urls::Urls::new(base_url).reports().base().to_string(), + }, + ] +} + +fn active_navbar_item_id(mut url: Url) -> u8 { + match url.remaining_hash_path_parts().as_slice() { + [] => 10, + [constants::DASHBOARD] => 0, + [constants::TEAM] => 1, + [constants::PROJECTS] => 2, + [constants::CALENDAR] => 3, + [constants::REPORTS] => 4, + _ => 10, + } +} + +fn profile_menu() -> Vec { + vec![ + models::NavigationItem { + id: 0, + name: "Your Profile".to_string(), + href: "#".to_string(), + }, + models::NavigationItem { + id: 1, + name: "Settings".to_string(), + href: "#".to_string(), + }, + models::NavigationItem { + id: 2, + name: "Sign out".to_string(), + href: "#".to_string(), + }, + ] +} diff --git a/frontend/src/models.rs b/frontend/src/models.rs index ef8f178..277a271 100644 --- a/frontend/src/models.rs +++ b/frontend/src/models.rs @@ -1,35 +1,35 @@ -use seed::prelude::*; -use serde::{Deserialize, Serialize}; - -use crate::pages; - -// `Model` describes our app state. -pub struct Model { - pub ctx: Context, - pub url: Url, - pub page: pages::Pages, - pub is_dark_mode: bool, - pub navbar: Vec, - pub navbar_active_item_id: u8, - pub navbar_hamburger_menu_visible: bool, - pub profile_menu: Vec, - pub profile_menu_visible: bool, - pub secret_message: Option, -} - -pub struct Context { - pub user: Option, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct User { - pub username: String, - pub email: Option, - pub token: String, -} - -pub struct NavigationItem { - pub id: u8, - pub name: String, - pub href: String, -} +use seed::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::pages; + +// `Model` describes our app state. +pub struct Model { + pub ctx: Context, + pub url: Url, + pub page: pages::Pages, + pub is_dark_mode: bool, + pub navbar: Vec, + pub navbar_active_item_id: u8, + pub navbar_hamburger_menu_visible: bool, + pub profile_menu: Vec, + pub profile_menu_visible: bool, + pub secret_message: Option, +} + +pub struct Context { + pub user: Option, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct User { + pub username: String, + pub email: Option, + pub token: String, +} + +pub struct NavigationItem { + pub id: u8, + pub name: String, + pub href: String, +} diff --git a/frontend/src/pages/authentication/components/sign_in.rs b/frontend/src/pages/authentication/components/sign_in.rs index 3ef50da..b3ec52a 100644 --- a/frontend/src/pages/authentication/components/sign_in.rs +++ b/frontend/src/pages/authentication/components/sign_in.rs @@ -1,260 +1,260 @@ -use seed::{*, prelude::*}; - -use crate::pages::authentication::updates; - -pub fn view_sign_in_section(base_url: &Url) -> Node { - section![ - C!["bg-gray-50", "dark:bg-gray-900"], - div![ - C![ - "flex", - "flex-col", - "items-center", - "justify-center", - "px-6", - "py-8", - "mx-auto", - "md:h-screen", - "lg:py-0" - ], - a![ - C![ - "flex", - "items-center", - "mb-6", - "text-2xl", - "font-semibold", - "text-gray-900", - "dark:text-white" - ], - attrs! { - At::Href => base_url, - }, - img![ - C!["w-8", "h-8", "mr-2"], - attrs! { - At::Src => "/assets/icons/logo.svg", - At::Alt => "Connect Your Books Logo" - }, - ], - "Connect Your Books", - ], - div![ - C![ - "w-full", - "bg-white", - "rounded-lg", - "shadow", - "dark:border", - "md:mt-0", - "sm:max-w-md", - "xl:p-0", - "dark:bg-gray-800", - "dark:border-gray-700" - ], - div![ - C!["p-6", "space-y-4", "md:space-y-6", "sm:p-8"], - h1![ - C![ - "text-xl", - "font-bold", - "leading-tight", - "tracking-tight", - "text-gray-900", - "md:text-2xl", - "dark:text-white" - ], - "Sign in to your account", - ], - form![ - C!["space-y-4", "md:space-y-6"], - div![ - label![ - C![ - "block", - "mb-2", - "text-sm", - "font-medium", - "text-gray-900", - "dark:text-white" - ], - attrs! { - At::For => "username", - }, - "Your username", - ], - input![ - C![ - "bg-gray-50", - "border", - "border-gray-300", - "text-gray-900", - "sm:text-sm", - "rounded-lg", - "focus:ring-primary-600", - "focus:border-primary-600" - "block", - "w-full", - "p-2.5", - "dark:bg-gray-700", - "dark:border-gray-600", - "dark:placeholder-gray-400", - "dark:text-white dark:focus:ring-blue-500", - "dark:focus:border-blue-500" - ], - attrs! { - At::Id => "username", - At::Name => "username", - At::Type => "text", - At::Placeholder => "User Name", - At::Required => "", - }, - ], - ], - div![ - label![ - C![ - "block", - "mb-2", - "text-sm", - "font-medium", - "text-gray-900", - "dark:text-white" - ], - attrs! { - At::For => "password", - }, - "Password", - ], - input![ - C![ - "bg-gray-50", - "border", - "border-gray-300", - "text-gray-900", - "sm:text-sm", - "rounded-lg", - "focus:ring-primary-600", - "focus:border-primary-600" - "block", - "w-full", - "p-2.5", - "dark:bg-gray-700", - "dark:border-gray-600", - "dark:placeholder-gray-400", - "dark:text-white dark:focus:ring-blue-500", - "dark:focus:border-blue-500" - ], - attrs! { - At::Id => "password", - At::Name => "password", - At::Type => "password", - At::Placeholder => "••••••••", - At::Required => "", - }, - ], - ], - div![ - C!["flex", "items-center", "justify-between"], - div![ - C!["flex", "items-start"], - div![ - C!["flex", "items-center", "h-5"], - input![ - C![ - "w-4", - "h-4", - "border", - "border-gray-300", - "rounded", - "bg-gray-50", - "focus:ring-3", - "focus:ring-primary-300", - "dark:bg-gray-700", - "dark:border-gray-600", - "dark:focus:ring-primary-600", - "dark:ring-offset-gray-800" - ], - attrs! { - At::Id => "remember", - At::Type => "checkbox", - At::AriaDescribedBy => "remember", - }, - ], - ], - div![ - C!["ml-3", "text-sm"], - label![ - C!["text-gray-500", "dark:text-gray-300"], - attrs! { - At::For => "remember", - }, - "Remember me", - ], - ], - ], - a![ - C![ - "text-sm", - "font-medium", - "text-primary-600", - "hover:underline", - "dark:text-primary-500" - ], - attrs! { - At::Href => "#", - }, - "Forgot password?", - ], - ], - button![ - C![ - "w-full", - "text-white", - "bg-primary-600", - "hover:bg-primary-700", - "focus:ring-4", - "focus:outline-none", - "focus:ring-primary-300", - "font-medium", - "rounded-lg", - "text-sm", - "px-5", - "py-2.5", - "text-center", - "dark:bg-primary-600", - "dark:hover:bg-primary-700", - "dark:focus:ring-primary-800" - ], - attrs! { - At::Type => "submit", - }, - "Sign in", - ], - p![ - C![ - "text-sm", - "font-light", - "text-gray-500", - "dark:text-gray-400" - ], - "Don’t have an account yet? ", - a![ - C![ - "font-medium", - "text-primary-600", - "hover:underline", - "dark:text-primary-500" - ], - attrs! { - At::Href => "#", - }, - "Sign up", - ], - ], - ], - ], - ], - ], - ] -} +use seed::{*, prelude::*}; + +use crate::pages::authentication::updates; + +pub fn view_sign_in_section(base_url: &Url) -> Node { + section![ + C!["bg-gray-50", "dark:bg-gray-900"], + div![ + C![ + "flex", + "flex-col", + "items-center", + "justify-center", + "px-6", + "py-8", + "mx-auto", + "md:h-screen", + "lg:py-0" + ], + a![ + C![ + "flex", + "items-center", + "mb-6", + "text-2xl", + "font-semibold", + "text-gray-900", + "dark:text-white" + ], + attrs! { + At::Href => base_url, + }, + img![ + C!["w-8", "h-8", "mr-2"], + attrs! { + At::Src => "/assets/icons/logo.svg", + At::Alt => "Connect Your Books Logo" + }, + ], + "Connect Your Books", + ], + div![ + C![ + "w-full", + "bg-white", + "rounded-lg", + "shadow", + "dark:border", + "md:mt-0", + "sm:max-w-md", + "xl:p-0", + "dark:bg-gray-800", + "dark:border-gray-700" + ], + div![ + C!["p-6", "space-y-4", "md:space-y-6", "sm:p-8"], + h1![ + C![ + "text-xl", + "font-bold", + "leading-tight", + "tracking-tight", + "text-gray-900", + "md:text-2xl", + "dark:text-white" + ], + "Sign in to your account", + ], + form![ + C!["space-y-4", "md:space-y-6"], + div![ + label![ + C![ + "block", + "mb-2", + "text-sm", + "font-medium", + "text-gray-900", + "dark:text-white" + ], + attrs! { + At::For => "username", + }, + "Your username", + ], + input![ + C![ + "bg-gray-50", + "border", + "border-gray-300", + "text-gray-900", + "sm:text-sm", + "rounded-lg", + "focus:ring-primary-600", + "focus:border-primary-600" + "block", + "w-full", + "p-2.5", + "dark:bg-gray-700", + "dark:border-gray-600", + "dark:placeholder-gray-400", + "dark:text-white dark:focus:ring-blue-500", + "dark:focus:border-blue-500" + ], + attrs! { + At::Id => "username", + At::Name => "username", + At::Type => "text", + At::Placeholder => "User Name", + At::Required => "", + }, + ], + ], + div![ + label![ + C![ + "block", + "mb-2", + "text-sm", + "font-medium", + "text-gray-900", + "dark:text-white" + ], + attrs! { + At::For => "password", + }, + "Password", + ], + input![ + C![ + "bg-gray-50", + "border", + "border-gray-300", + "text-gray-900", + "sm:text-sm", + "rounded-lg", + "focus:ring-primary-600", + "focus:border-primary-600" + "block", + "w-full", + "p-2.5", + "dark:bg-gray-700", + "dark:border-gray-600", + "dark:placeholder-gray-400", + "dark:text-white dark:focus:ring-blue-500", + "dark:focus:border-blue-500" + ], + attrs! { + At::Id => "password", + At::Name => "password", + At::Type => "password", + At::Placeholder => "••••••••", + At::Required => "", + }, + ], + ], + div![ + C!["flex", "items-center", "justify-between"], + div![ + C!["flex", "items-start"], + div![ + C!["flex", "items-center", "h-5"], + input![ + C![ + "w-4", + "h-4", + "border", + "border-gray-300", + "rounded", + "bg-gray-50", + "focus:ring-3", + "focus:ring-primary-300", + "dark:bg-gray-700", + "dark:border-gray-600", + "dark:focus:ring-primary-600", + "dark:ring-offset-gray-800" + ], + attrs! { + At::Id => "remember", + At::Type => "checkbox", + At::AriaDescribedBy => "remember", + }, + ], + ], + div![ + C!["ml-3", "text-sm"], + label![ + C!["text-gray-500", "dark:text-gray-300"], + attrs! { + At::For => "remember", + }, + "Remember me", + ], + ], + ], + a![ + C![ + "text-sm", + "font-medium", + "text-primary-600", + "hover:underline", + "dark:text-primary-500" + ], + attrs! { + At::Href => "#", + }, + "Forgot password?", + ], + ], + button![ + C![ + "w-full", + "text-white", + "bg-primary-600", + "hover:bg-primary-700", + "focus:ring-4", + "focus:outline-none", + "focus:ring-primary-300", + "font-medium", + "rounded-lg", + "text-sm", + "px-5", + "py-2.5", + "text-center", + "dark:bg-primary-600", + "dark:hover:bg-primary-700", + "dark:focus:ring-primary-800" + ], + attrs! { + At::Type => "submit", + }, + "Sign in", + ], + p![ + C![ + "text-sm", + "font-light", + "text-gray-500", + "dark:text-gray-400" + ], + "Don’t have an account yet? ", + a![ + C![ + "font-medium", + "text-primary-600", + "hover:underline", + "dark:text-primary-500" + ], + attrs! { + At::Href => "#", + }, + "Sign up", + ], + ], + ], + ], + ], + ], + ] +} diff --git a/frontend/src/pages/authentication/inits.rs b/frontend/src/pages/authentication/inits.rs index e6ff9fb..d372b0f 100644 --- a/frontend/src/pages/authentication/inits.rs +++ b/frontend/src/pages/authentication/inits.rs @@ -1,9 +1,9 @@ -use seed::prelude::*; - -use crate::pages::authentication::models; -use crate::pages::authentication::updates; - -// `init` describes what should happen when your app started. -pub fn init(url: Url, orders: &mut impl Orders) -> models::Model { - models::Model { base_url: url } +use seed::prelude::*; + +use crate::pages::authentication::models; +use crate::pages::authentication::updates; + +// `init` describes what should happen when your app started. +pub fn init(url: Url, orders: &mut impl Orders) -> models::Model { + models::Model { base_url: url } } \ No newline at end of file diff --git a/frontend/src/pages/authentication/mod.rs b/frontend/src/pages/authentication/mod.rs index 2c43fff..2c40009 100644 --- a/frontend/src/pages/authentication/mod.rs +++ b/frontend/src/pages/authentication/mod.rs @@ -1,6 +1,6 @@ -pub mod components; -pub mod views; -pub mod urls; -pub mod updates; -pub mod models; +pub mod components; +pub mod views; +pub mod urls; +pub mod updates; +pub mod models; pub mod inits; \ No newline at end of file diff --git a/frontend/src/pages/authentication/models.rs b/frontend/src/pages/authentication/models.rs index 6970946..096bf0e 100644 --- a/frontend/src/pages/authentication/models.rs +++ b/frontend/src/pages/authentication/models.rs @@ -1,6 +1,6 @@ -use seed::prelude::*; - -// `Model` describes our app state. -pub struct Model { - pub base_url: Url, -} +use seed::prelude::*; + +// `Model` describes our app state. +pub struct Model { + pub base_url: Url, +} diff --git a/frontend/src/pages/authentication/updates.rs b/frontend/src/pages/authentication/updates.rs index d65f93c..0ba4c17 100644 --- a/frontend/src/pages/authentication/updates.rs +++ b/frontend/src/pages/authentication/updates.rs @@ -1,9 +1,9 @@ -use seed::prelude::*; - -use crate::pages::authentication::models; - -// `Msg` describes the different events you can modify state with. -pub enum Msg {} - -// `update` describes how to handle each `Msg`. +use seed::prelude::*; + +use crate::pages::authentication::models; + +// `Msg` describes the different events you can modify state with. +pub enum Msg {} + +// `update` describes how to handle each `Msg`. pub fn update(msg: Msg, model: &mut models::Model, orders: &mut impl Orders) {} \ No newline at end of file diff --git a/frontend/src/pages/authentication/urls.rs b/frontend/src/pages/authentication/urls.rs index 9700764..dced8ae 100644 --- a/frontend/src/pages/authentication/urls.rs +++ b/frontend/src/pages/authentication/urls.rs @@ -1,9 +1,9 @@ -use seed::*; - -struct_urls!(); - -impl<'a> Urls<'a> { - pub fn base(self) -> Url { - self.base_url() - } -} +use seed::*; + +struct_urls!(); + +impl<'a> Urls<'a> { + pub fn base(self) -> Url { + self.base_url() + } +} diff --git a/frontend/src/pages/authentication/views.rs b/frontend/src/pages/authentication/views.rs index 0bc98ac..ec878a2 100644 --- a/frontend/src/pages/authentication/views.rs +++ b/frontend/src/pages/authentication/views.rs @@ -1,12 +1,12 @@ -use seed::{*, prelude::*}; - -use crate::pages::authentication::components; -use crate::pages::authentication::models; -use crate::pages::authentication::updates; - -pub fn view(model: &models::Model) -> Vec> { - vec![main![ - C!["bg-white", "dark:bg-gray-900"], - components::sign_in::view_sign_in_section(&model.base_url), - ]] -} +use seed::{*, prelude::*}; + +use crate::pages::authentication::components; +use crate::pages::authentication::models; +use crate::pages::authentication::updates; + +pub fn view(model: &models::Model) -> Vec> { + vec![main![ + C!["bg-white", "dark:bg-gray-900"], + components::sign_in::view_sign_in_section(&model.base_url), + ]] +} diff --git a/frontend/src/pages/home/inits.rs b/frontend/src/pages/home/inits.rs index 1ae75bd..72390e6 100644 --- a/frontend/src/pages/home/inits.rs +++ b/frontend/src/pages/home/inits.rs @@ -1,69 +1,69 @@ -use seed::prelude::*; - -use crate::components; -use crate::pages::home::models; -use crate::pages::home::updates; - -// `init` describes what should happen when your app started. -pub fn init(url: Url, orders: &mut impl Orders) -> models::Model { - models::Model { team: init_team() } -} - -fn init_team() -> Vec { - vec![ - models::TeamMember { - name: "Michiel de Jong".to_string(), - role: "Code Communist".to_string(), - description: "Michiel de Jong is an independent freedom hacker, travelling the world \ - while coding for the revolution from his laptop. " - .to_string(), - image_url: "/assets/images/vendor/michiel_dejong.avif".to_string(), - links: Some(vec![ - models::TeamMemberLink { - logo: Some(components::svg::logo_linkedin().to_string()), - url: Some("https://www.linkedin.com/in/michielbdejong".to_string()), - }, - models::TeamMemberLink { - logo: Some(components::svg::logo_github().to_string()), - url: Some("https://github.com/michielbdejong".to_string()), - }, - ]), - }, - models::TeamMember { - name: "Mahdi Baghbani".to_string(), - role: "Percussive Maintainer".to_string(), - description: "Don’t tell your IT staff I said this… but there’s nothing like a good \ - whack to fix a malfunctioning machine." - .to_string(), - image_url: "/assets/images/vendor/mahdi_baghbani.avif".to_string(), - links: Some(vec![ - models::TeamMemberLink { - logo: Some(components::svg::logo_linkedin().to_string()), - url: Some("https://www.linkedin.com/in/mahdibaghbani".to_string()), - }, - models::TeamMemberLink { - logo: Some(components::svg::logo_github().to_string()), - url: Some("https://github.com/MahdiBaghbani".to_string()), - }, - ]), - }, - models::TeamMember { - name: "Yashar PourMohammad".to_string(), - role: "Code Guru".to_string(), - description: "Yashar is keen to understand the inner workings of mind, code and \ - society. He thinks too much and codes too little." - .to_string(), - image_url: "/assets/images/vendor/yashar_pourmohammad.avif".to_string(), - links: Some(vec![ - models::TeamMemberLink { - logo: Some(components::svg::logo_linkedin().to_string()), - url: Some("https://www.linkedin.com/in/yasharpm".to_string()), - }, - models::TeamMemberLink { - logo: Some(components::svg::logo_github().to_string()), - url: Some("https://github.com/yasharpm".to_string()), - }, - ]), - }, - ] +use seed::prelude::*; + +use crate::components; +use crate::pages::home::models; +use crate::pages::home::updates; + +// `init` describes what should happen when your app started. +pub fn init(url: Url, orders: &mut impl Orders) -> models::Model { + models::Model { team: init_team() } +} + +fn init_team() -> Vec { + vec![ + models::TeamMember { + name: "Michiel de Jong".to_string(), + role: "Code Communist".to_string(), + description: "Michiel de Jong is an independent freedom hacker, travelling the world \ + while coding for the revolution from his laptop. " + .to_string(), + image_url: "/assets/images/vendor/michiel_dejong.avif".to_string(), + links: Some(vec![ + models::TeamMemberLink { + logo: Some(components::svg::logo_linkedin().to_string()), + url: Some("https://www.linkedin.com/in/michielbdejong".to_string()), + }, + models::TeamMemberLink { + logo: Some(components::svg::logo_github().to_string()), + url: Some("https://github.com/michielbdejong".to_string()), + }, + ]), + }, + models::TeamMember { + name: "Mahdi Baghbani".to_string(), + role: "Percussive Maintainer".to_string(), + description: "Don’t tell your IT staff I said this… but there’s nothing like a good \ + whack to fix a malfunctioning machine." + .to_string(), + image_url: "/assets/images/vendor/mahdi_baghbani.avif".to_string(), + links: Some(vec![ + models::TeamMemberLink { + logo: Some(components::svg::logo_linkedin().to_string()), + url: Some("https://www.linkedin.com/in/mahdibaghbani".to_string()), + }, + models::TeamMemberLink { + logo: Some(components::svg::logo_github().to_string()), + url: Some("https://github.com/MahdiBaghbani".to_string()), + }, + ]), + }, + models::TeamMember { + name: "Yashar PourMohammad".to_string(), + role: "Code Guru".to_string(), + description: "Yashar is keen to understand the inner workings of mind, code and \ + society. He thinks too much and codes too little." + .to_string(), + image_url: "/assets/images/vendor/yashar_pourmohammad.avif".to_string(), + links: Some(vec![ + models::TeamMemberLink { + logo: Some(components::svg::logo_linkedin().to_string()), + url: Some("https://www.linkedin.com/in/yasharpm".to_string()), + }, + models::TeamMemberLink { + logo: Some(components::svg::logo_github().to_string()), + url: Some("https://github.com/yasharpm".to_string()), + }, + ]), + }, + ] } \ No newline at end of file diff --git a/frontend/src/pages/home/mod.rs b/frontend/src/pages/home/mod.rs index eb9def4..a15d2ea 100644 --- a/frontend/src/pages/home/mod.rs +++ b/frontend/src/pages/home/mod.rs @@ -1,6 +1,6 @@ -pub mod components; -pub mod views; -pub mod urls; -pub mod updates; -pub mod models; -pub mod inits; +pub mod components; +pub mod views; +pub mod urls; +pub mod updates; +pub mod models; +pub mod inits; diff --git a/frontend/src/pages/home/models.rs b/frontend/src/pages/home/models.rs index f084dfc..80f5c5e 100644 --- a/frontend/src/pages/home/models.rs +++ b/frontend/src/pages/home/models.rs @@ -1,19 +1,19 @@ -// `Model` describes our app state. -pub struct Model { - pub team: Vec, -} - -#[derive(Clone, Debug)] -pub struct TeamMember { - pub name: String, - pub role: String, - pub description: String, - pub image_url: String, - pub links: Option>, -} - -#[derive(Clone, Debug)] -pub struct TeamMemberLink { - pub logo: Option, - pub url: Option, -} +// `Model` describes our app state. +pub struct Model { + pub team: Vec, +} + +#[derive(Clone, Debug)] +pub struct TeamMember { + pub name: String, + pub role: String, + pub description: String, + pub image_url: String, + pub links: Option>, +} + +#[derive(Clone, Debug)] +pub struct TeamMemberLink { + pub logo: Option, + pub url: Option, +} diff --git a/frontend/src/pages/home/updates.rs b/frontend/src/pages/home/updates.rs index 300d13f..977bbf8 100644 --- a/frontend/src/pages/home/updates.rs +++ b/frontend/src/pages/home/updates.rs @@ -1,8 +1,8 @@ -use seed::prelude::*; - -use crate::pages::home::models; - -pub enum Msg {} - -// `update` describes how to handle each `Msg`. -pub fn update(msg: Msg, model: &mut models::Model, orders: &mut impl Orders) {} +use seed::prelude::*; + +use crate::pages::home::models; + +pub enum Msg {} + +// `update` describes how to handle each `Msg`. +pub fn update(msg: Msg, model: &mut models::Model, orders: &mut impl Orders) {} diff --git a/frontend/src/pages/home/urls.rs b/frontend/src/pages/home/urls.rs index 57f4fea..23bd1da 100644 --- a/frontend/src/pages/home/urls.rs +++ b/frontend/src/pages/home/urls.rs @@ -1,10 +1,10 @@ -use seed::{*}; - -struct_urls!(); - -impl<'a> Urls<'a> { - pub fn base(self) -> Url { - self.base_url() - } -} - +use seed::{*}; + +struct_urls!(); + +impl<'a> Urls<'a> { + pub fn base(self) -> Url { + self.base_url() + } +} + diff --git a/frontend/src/pages/home/views.rs b/frontend/src/pages/home/views.rs index 0f92fc9..c5d392f 100644 --- a/frontend/src/pages/home/views.rs +++ b/frontend/src/pages/home/views.rs @@ -1,15 +1,15 @@ -use seed::{*, prelude::*}; - -use crate::pages::home::components; -use crate::pages::home::models; -use crate::pages::home::updates; - -pub fn view(model: &models::Model) -> Vec> { - vec![main![ - C!["bg-white", "dark:bg-gray-900"], - components::hero::view_hero_section(), - components::features::view_feature_list(), - components::cta::view_call_to_action(), - components::team::view_our_team(&model.team), - ]] -} +use seed::{*, prelude::*}; + +use crate::pages::home::components; +use crate::pages::home::models; +use crate::pages::home::updates; + +pub fn view(model: &models::Model) -> Vec> { + vec![main![ + C!["bg-white", "dark:bg-gray-900"], + components::hero::view_hero_section(), + components::features::view_feature_list(), + components::cta::view_call_to_action(), + components::team::view_our_team(&model.team), + ]] +} diff --git a/frontend/src/updates.rs b/frontend/src/updates.rs index 83a09e6..260370f 100644 --- a/frontend/src/updates.rs +++ b/frontend/src/updates.rs @@ -1,91 +1,91 @@ -use seed::prelude::*; - -use crate::models; -use crate::pages; - -// `Msg` describes the different events you can modify state with. -pub enum Msg { - UrlChanged(subs::UrlChanged), - ToggleDarkMode, - ChangeNavBarActiveItem(u8), - ToggleNavBarHamburgerView, - ToggleProfileMenu, - HideProfileMenu, - TopSecretFetched(Result), - - // ------ pages ------ - HomeMsg(pages::home::updates::Msg), - DashboardMsg(pages::dashboard::Msg), - TeamMsg(pages::team::Msg), - ProjectsMsg(pages::projects::Msg), - CalendarMsg(pages::calendar::Msg), - ReportsMsg(pages::reports::Msg), - AuthenticationMsg(pages::authentication::updates::Msg), -} - -// `update` describes how to handle each `Msg`. -pub fn update(msg: Msg, model: &mut models::Model, orders: &mut impl Orders) { - match msg { - Msg::UrlChanged(subs::UrlChanged(url)) => { - model.page = pages::Pages::init(url, orders, model.ctx.user.as_ref()); - } - Msg::ChangeNavBarActiveItem(id) => { - model.navbar_active_item_id = id; - } - Msg::ToggleDarkMode => model.is_dark_mode = not(model.is_dark_mode), - Msg::ToggleNavBarHamburgerView => { - model.navbar_hamburger_menu_visible = not(model.navbar_hamburger_menu_visible) - } - Msg::ToggleProfileMenu => { - model.profile_menu_visible = not(model.profile_menu_visible); - } - Msg::HideProfileMenu => { - if model.profile_menu_visible { - model.profile_menu_visible = false; - } else { - orders.skip(); - } - } - Msg::TopSecretFetched(Ok(secret_message)) => { - model.secret_message = Some(secret_message); - } - - // ------ pages ------ - Msg::HomeMsg(msg) => { - if let pages::Pages::Home(model) = &mut model.page { - pages::home::updates::update(msg, model, &mut orders.proxy(Msg::HomeMsg)) - } - } - Msg::DashboardMsg(msg) => { - if let pages::Pages::Dashboard(model) = &mut model.page { - pages::dashboard::update(msg, model, &mut orders.proxy(Msg::DashboardMsg)) - } - } - Msg::TeamMsg(msg) => { - if let pages::Pages::Team(model) = &mut model.page { - pages::team::update(msg, model, &mut orders.proxy(Msg::TeamMsg)) - } - } - Msg::ProjectsMsg(msg) => { - if let pages::Pages::Projects(model) = &mut model.page { - pages::projects::update(msg, model, &mut orders.proxy(Msg::ProjectsMsg)) - } - } - Msg::CalendarMsg(msg) => { - if let pages::Pages::Calendar(model) = &mut model.page { - pages::calendar::update(msg, model, &mut orders.proxy(Msg::CalendarMsg)) - } - } - Msg::ReportsMsg(msg) => { - if let pages::Pages::Reports(model) = &mut model.page { - pages::reports::update(msg, model, &mut orders.proxy(Msg::ReportsMsg)) - } - } - Msg::AuthenticationMsg(msg) => { - if let pages::Pages::Authentication(model) = &mut model.page { - pages::authentication::updates::update(msg, model, &mut orders.proxy(Msg::AuthenticationMsg)) - } - } - _ => {} - } -} +use seed::prelude::*; + +use crate::models; +use crate::pages; + +// `Msg` describes the different events you can modify state with. +pub enum Msg { + UrlChanged(subs::UrlChanged), + ToggleDarkMode, + ChangeNavBarActiveItem(u8), + ToggleNavBarHamburgerView, + ToggleProfileMenu, + HideProfileMenu, + TopSecretFetched(Result), + + // ------ pages ------ + HomeMsg(pages::home::updates::Msg), + DashboardMsg(pages::dashboard::Msg), + TeamMsg(pages::team::Msg), + ProjectsMsg(pages::projects::Msg), + CalendarMsg(pages::calendar::Msg), + ReportsMsg(pages::reports::Msg), + AuthenticationMsg(pages::authentication::updates::Msg), +} + +// `update` describes how to handle each `Msg`. +pub fn update(msg: Msg, model: &mut models::Model, orders: &mut impl Orders) { + match msg { + Msg::UrlChanged(subs::UrlChanged(url)) => { + model.page = pages::Pages::init(url, orders, model.ctx.user.as_ref()); + } + Msg::ChangeNavBarActiveItem(id) => { + model.navbar_active_item_id = id; + } + Msg::ToggleDarkMode => model.is_dark_mode = not(model.is_dark_mode), + Msg::ToggleNavBarHamburgerView => { + model.navbar_hamburger_menu_visible = not(model.navbar_hamburger_menu_visible) + } + Msg::ToggleProfileMenu => { + model.profile_menu_visible = not(model.profile_menu_visible); + } + Msg::HideProfileMenu => { + if model.profile_menu_visible { + model.profile_menu_visible = false; + } else { + orders.skip(); + } + } + Msg::TopSecretFetched(Ok(secret_message)) => { + model.secret_message = Some(secret_message); + } + + // ------ pages ------ + Msg::HomeMsg(msg) => { + if let pages::Pages::Home(model) = &mut model.page { + pages::home::updates::update(msg, model, &mut orders.proxy(Msg::HomeMsg)) + } + } + Msg::DashboardMsg(msg) => { + if let pages::Pages::Dashboard(model) = &mut model.page { + pages::dashboard::update(msg, model, &mut orders.proxy(Msg::DashboardMsg)) + } + } + Msg::TeamMsg(msg) => { + if let pages::Pages::Team(model) = &mut model.page { + pages::team::update(msg, model, &mut orders.proxy(Msg::TeamMsg)) + } + } + Msg::ProjectsMsg(msg) => { + if let pages::Pages::Projects(model) = &mut model.page { + pages::projects::update(msg, model, &mut orders.proxy(Msg::ProjectsMsg)) + } + } + Msg::CalendarMsg(msg) => { + if let pages::Pages::Calendar(model) = &mut model.page { + pages::calendar::update(msg, model, &mut orders.proxy(Msg::CalendarMsg)) + } + } + Msg::ReportsMsg(msg) => { + if let pages::Pages::Reports(model) = &mut model.page { + pages::reports::update(msg, model, &mut orders.proxy(Msg::ReportsMsg)) + } + } + Msg::AuthenticationMsg(msg) => { + if let pages::Pages::Authentication(model) = &mut model.page { + pages::authentication::updates::update(msg, model, &mut orders.proxy(Msg::AuthenticationMsg)) + } + } + _ => {} + } +} diff --git a/frontend/src/urls.rs b/frontend/src/urls.rs index 21b3408..64295f8 100644 --- a/frontend/src/urls.rs +++ b/frontend/src/urls.rs @@ -1,33 +1,33 @@ -use seed::{*}; - -use crate::constants; -use crate::pages; - -struct_urls!(); - -impl<'a> Urls<'a> { - pub fn home(self) -> Url { - self.base_url() - } - pub fn dashboard(self) -> pages::dashboard::Urls<'a> { - pages::dashboard::Urls::new(self.base_url().add_hash_path_part(constants::DASHBOARD)) - } - pub fn team(self) -> pages::team::Urls<'a> { - pages::team::Urls::new(self.base_url().add_hash_path_part(constants::TEAM)) - } - pub fn projects(self) -> pages::projects::Urls<'a> { - pages::projects::Urls::new(self.base_url().add_hash_path_part(constants::PROJECTS)) - } - pub fn calendar(self) -> pages::calendar::Urls<'a> { - pages::calendar::Urls::new(self.base_url().add_hash_path_part(constants::CALENDAR)) - } - pub fn reports(self) -> pages::reports::Urls<'a> { - pages::reports::Urls::new(self.base_url().add_hash_path_part(constants::REPORTS)) - } - pub fn authentication(self) -> pages::authentication::urls::Urls<'a> { - pages::authentication::urls::Urls::new( - self.base_url() - .add_hash_path_part(constants::AUTHENTICATION), - ) - } -} +use seed::{*}; + +use crate::constants; +use crate::pages; + +struct_urls!(); + +impl<'a> Urls<'a> { + pub fn home(self) -> Url { + self.base_url() + } + pub fn dashboard(self) -> pages::dashboard::Urls<'a> { + pages::dashboard::Urls::new(self.base_url().add_hash_path_part(constants::DASHBOARD)) + } + pub fn team(self) -> pages::team::Urls<'a> { + pages::team::Urls::new(self.base_url().add_hash_path_part(constants::TEAM)) + } + pub fn projects(self) -> pages::projects::Urls<'a> { + pages::projects::Urls::new(self.base_url().add_hash_path_part(constants::PROJECTS)) + } + pub fn calendar(self) -> pages::calendar::Urls<'a> { + pages::calendar::Urls::new(self.base_url().add_hash_path_part(constants::CALENDAR)) + } + pub fn reports(self) -> pages::reports::Urls<'a> { + pages::reports::Urls::new(self.base_url().add_hash_path_part(constants::REPORTS)) + } + pub fn authentication(self) -> pages::authentication::urls::Urls<'a> { + pages::authentication::urls::Urls::new( + self.base_url() + .add_hash_path_part(constants::AUTHENTICATION), + ) + } +} diff --git a/frontend/src/views.rs b/frontend/src/views.rs index 5a4d7f5..12293ec 100644 --- a/frontend/src/views.rs +++ b/frontend/src/views.rs @@ -1,38 +1,38 @@ -use seed::{*, prelude::*}; - -use crate::components; -use crate::models; -use crate::pages; -use crate::updates; - -pub fn view(model: &models::Model) -> Vec> { - vec![div![ - IF!(model.is_dark_mode => C!["dark"]), - C!["flex", "flex-col", "min-h-full"], - components::navbar::view_navbar(model, &model.url, model.ctx.user.as_ref(),), - view_content(&model.page), - components::footer::view_footer(), - ]] -} - -// ----- view_content ------ -fn view_content(page: &pages::Pages) -> Vec> { - match page { - pages::Pages::Home(model) => pages::home::views::view(model).map_msg(updates::Msg::HomeMsg), - pages::Pages::Dashboard(_model) => { - pages::dashboard::view().map_msg(updates::Msg::DashboardMsg) - } - pages::Pages::Team(_model) => pages::team::view().map_msg(updates::Msg::TeamMsg), - pages::Pages::Projects(_model) => { - pages::projects::view().map_msg(updates::Msg::ProjectsMsg) - } - pages::Pages::Calendar(_model) => { - pages::calendar::view().map_msg(updates::Msg::CalendarMsg) - } - pages::Pages::Reports(_model) => pages::reports::view().map_msg(updates::Msg::ReportsMsg), - pages::Pages::Authentication(model) => { - pages::authentication::views::view(model).map_msg(updates::Msg::AuthenticationMsg) - } - pages::Pages::NotFound => pages::dashboard::view().map_msg(updates::Msg::DashboardMsg), - } -} +use seed::{*, prelude::*}; + +use crate::components; +use crate::models; +use crate::pages; +use crate::updates; + +pub fn view(model: &models::Model) -> Vec> { + vec![div![ + IF!(model.is_dark_mode => C!["dark"]), + C!["flex", "flex-col", "min-h-full"], + components::navbar::view_navbar(model, &model.url, model.ctx.user.as_ref(),), + view_content(&model.page), + components::footer::view_footer(), + ]] +} + +// ----- view_content ------ +fn view_content(page: &pages::Pages) -> Vec> { + match page { + pages::Pages::Home(model) => pages::home::views::view(model).map_msg(updates::Msg::HomeMsg), + pages::Pages::Dashboard(_model) => { + pages::dashboard::view().map_msg(updates::Msg::DashboardMsg) + } + pages::Pages::Team(_model) => pages::team::view().map_msg(updates::Msg::TeamMsg), + pages::Pages::Projects(_model) => { + pages::projects::view().map_msg(updates::Msg::ProjectsMsg) + } + pages::Pages::Calendar(_model) => { + pages::calendar::view().map_msg(updates::Msg::CalendarMsg) + } + pages::Pages::Reports(_model) => pages::reports::view().map_msg(updates::Msg::ReportsMsg), + pages::Pages::Authentication(model) => { + pages::authentication::views::view(model).map_msg(updates::Msg::AuthenticationMsg) + } + pages::Pages::NotFound => pages::dashboard::view().map_msg(updates::Msg::DashboardMsg), + } +}