├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile.toml ├── README.md ├── apps ├── db-migration │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── main.rs │ │ └── utils │ │ ├── database.rs │ │ └── mod.rs └── server │ ├── Cargo.toml │ ├── Makefile.toml │ ├── src │ ├── app.rs │ ├── auth │ │ ├── auth_controller │ │ │ ├── get_tokens.rs │ │ │ └── mod.rs │ │ ├── auth_route.rs │ │ ├── errors.rs │ │ └── mod.rs │ ├── category │ │ ├── category_controller │ │ │ ├── create.rs │ │ │ ├── list.rs │ │ │ └── mod.rs │ │ ├── category_route.rs │ │ ├── errors.rs │ │ └── mod.rs │ ├── config │ │ ├── config_controller │ │ │ ├── get_config.rs │ │ │ └── mod.rs │ │ ├── config_route.rs │ │ ├── errors.rs │ │ └── mod.rs │ ├── errors.rs │ ├── lib.rs │ ├── main.rs │ ├── post │ │ ├── errors.rs │ │ ├── mod.rs │ │ ├── post_controller │ │ │ ├── create.rs │ │ │ ├── delete.rs │ │ │ ├── get.rs │ │ │ ├── mod.rs │ │ │ └── update.rs │ │ └── post_route.rs │ ├── user │ │ ├── dto │ │ │ ├── mod.rs │ │ │ └── user.rs │ │ ├── mod.rs │ │ ├── user_controller │ │ │ ├── get_me.rs │ │ │ ├── mod.rs │ │ │ └── update_user.rs │ │ └── user_route.rs │ └── utils │ │ ├── app_state.rs │ │ ├── auth.rs │ │ └── mod.rs │ └── tests │ ├── integration_test.rs │ └── integrations │ ├── category │ ├── create.rs │ ├── create_error_401.rs │ ├── create_error_403.rs │ ├── list.rs │ ├── list_403.rs │ └── mod.rs │ ├── config │ ├── get_config.rs │ └── mod.rs │ ├── health_check.rs │ ├── mod.rs │ ├── post │ ├── create.rs │ ├── delete.rs │ ├── mod.rs │ └── update.rs │ └── user │ ├── get_me.rs │ ├── get_me_401.rs │ ├── get_me_403.rs │ ├── mod.rs │ └── patch_user.rs ├── clippy.toml ├── codecov.yml ├── config ├── ci.toml ├── default.toml └── testing.toml ├── cspell.json ├── docker-compose.sample.yml ├── libs ├── .gitignore ├── actix-helmet │ ├── Cargo.toml │ ├── Makefile.toml │ ├── README.md │ └── src │ │ ├── builder.rs │ │ ├── inner.rs │ │ ├── lib.rs │ │ ├── middleware.rs │ │ └── policies.rs ├── core │ ├── Cargo.toml │ ├── Makefile.toml │ ├── src │ │ ├── constants.rs │ │ ├── lib.rs │ │ ├── migration │ │ │ ├── m_20221217_000001_create_user_table.rs │ │ │ ├── m_20230111_000002_create_file_table.rs │ │ │ ├── m_20230113_000003_create_category_table.rs │ │ │ ├── m_20230113_000004_create_post_table.rs │ │ │ ├── m_20230114_000005_create_user_role_column.rs │ │ │ ├── m_20230116_000006_create_post_reply_table.rs │ │ │ ├── m_20230124_000007_create_user_ban_history_table.rs │ │ │ ├── m_20230130_000008_create_default_categories.rs │ │ │ ├── m_20230131_000009_create_apps_version_table.rs │ │ │ ├── m_20230201_000010_create_config_table.rs │ │ │ └── mod.rs │ │ ├── repository │ │ │ ├── constants.rs │ │ │ ├── database │ │ │ │ ├── db_category.rs │ │ │ │ ├── db_config.rs │ │ │ │ ├── db_file.rs │ │ │ │ ├── db_post.rs │ │ │ │ ├── db_thread.rs │ │ │ │ ├── db_user.rs │ │ │ │ ├── entities │ │ │ │ │ ├── apps_version.rs │ │ │ │ │ ├── category.rs │ │ │ │ │ ├── config.rs │ │ │ │ │ ├── file.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── post.rs │ │ │ │ │ ├── post_reply.rs │ │ │ │ │ ├── prelude.rs │ │ │ │ │ ├── user.rs │ │ │ │ │ └── user_ban_history.rs │ │ │ │ └── mod.rs │ │ │ ├── errors.rs │ │ │ ├── mod.rs │ │ │ ├── repo_category.rs │ │ │ ├── repo_config.rs │ │ │ ├── repo_file.rs │ │ │ ├── repo_post.rs │ │ │ └── repo_user.rs │ │ ├── service │ │ │ ├── auth │ │ │ │ ├── auth_service.rs │ │ │ │ ├── constants.rs │ │ │ │ ├── errors.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── provider │ │ │ │ │ ├── api_provider.rs │ │ │ │ │ ├── errors.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── utils │ │ │ │ │ │ ├── get_tokens.rs │ │ │ │ │ │ ├── get_user_by_access_token.rs │ │ │ │ │ │ ├── mod.rs │ │ │ │ │ │ └── validate_access_token.rs │ │ │ │ └── utils │ │ │ │ │ ├── auth_user.rs │ │ │ │ │ ├── get_auth_user_by_access_token.rs │ │ │ │ │ ├── mock_data.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── validate_permission.rs │ │ │ │ │ └── validate_user.rs │ │ │ ├── category │ │ │ │ ├── category_service.rs │ │ │ │ ├── errors.rs │ │ │ │ ├── mod.rs │ │ │ │ └── utils │ │ │ │ │ ├── create.rs │ │ │ │ │ ├── delete.rs │ │ │ │ │ ├── find_by_id.rs │ │ │ │ │ ├── find_by_slug.rs │ │ │ │ │ ├── get_categories.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── update.rs │ │ │ ├── config │ │ │ │ ├── config_service.rs │ │ │ │ ├── constants.rs │ │ │ │ ├── errors.rs │ │ │ │ ├── mod.rs │ │ │ │ └── utils │ │ │ │ │ ├── get_config.rs │ │ │ │ │ └── mod.rs │ │ │ ├── factory.rs │ │ │ ├── file │ │ │ │ ├── constants.rs │ │ │ │ ├── errors.rs │ │ │ │ ├── file_service.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── provider │ │ │ │ │ ├── api_provider.rs │ │ │ │ │ ├── errors.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── utils │ │ │ │ │ │ ├── delete_file.rs │ │ │ │ │ │ ├── get_file.rs │ │ │ │ │ │ ├── get_upload_url.rs │ │ │ │ │ │ └── mod.rs │ │ │ │ └── utils │ │ │ │ │ ├── get_file.rs │ │ │ │ │ ├── get_upload_url.rs │ │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ ├── post │ │ │ │ ├── errors.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── post_service.rs │ │ │ │ └── utils │ │ │ │ │ ├── create.rs │ │ │ │ │ ├── create_reply.rs │ │ │ │ │ ├── delete.rs │ │ │ │ │ ├── delete_reply.rs │ │ │ │ │ ├── find_by_id.rs │ │ │ │ │ ├── find_reply_by_id.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── update.rs │ │ │ ├── prelude.rs │ │ │ └── user │ │ │ │ ├── constants.rs │ │ │ │ ├── errors.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── user_service.rs │ │ │ │ └── utils │ │ │ │ ├── ban_user_account.rs │ │ │ │ ├── delete_user_ban_history.rs │ │ │ │ ├── find_user_ban_history_by_id.rs │ │ │ │ ├── get_profile.rs │ │ │ │ ├── get_users.rs │ │ │ │ ├── mod.rs │ │ │ │ └── update_user_ban_history.rs │ │ └── utils │ │ │ ├── db_tools.rs │ │ │ ├── mock_data │ │ │ ├── apps_version.rs │ │ │ ├── mock_cognito_provider.rs │ │ │ ├── mock_s3_provider.rs │ │ │ ├── mod.rs │ │ │ └── post.rs │ │ │ └── mod.rs │ └── tests │ │ ├── unit │ │ ├── auth.rs │ │ ├── category.rs │ │ ├── config.rs │ │ ├── file.rs │ │ ├── migration.rs │ │ ├── mock_cognito_provider.rs │ │ ├── mod.rs │ │ ├── post.rs │ │ ├── post_reply.rs │ │ ├── user.rs │ │ └── user_ban_history.rs │ │ └── unit_test.rs └── utils │ ├── Cargo.toml │ ├── dict │ └── http-errors.json │ ├── src │ ├── amazon.rs │ ├── config.rs │ ├── db_connection.rs │ ├── http_errors.rs │ ├── lib.rs │ └── redis.rs │ └── tests │ ├── unit │ ├── db_connection.rs │ ├── http_errors.rs │ ├── mod.rs │ └── redis.rs │ └── unit_test.rs ├── makefile ├── notes └── 「Get」 Threads or 「List」 Threads?.md ├── rustfmt.toml └── tests ├── api-test.sh ├── postman-collections └── user_update_profile.json └── wait-for-readiness.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | indent_size = 4 7 | indent_style = tab 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false 10 | 11 | [*.yml] 12 | indent_size = 2 13 | indent_style = space 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/rust 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=rust 3 | 4 | ### Rust ### 5 | # Generated by Cargo 6 | # will have compiled files and executables 7 | debug/ 8 | target/ 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # End of https://www.toptal.com/developers/gitignore/api/rust 17 | 18 | db.sqlite 19 | 20 | .env 21 | .envrc 22 | 23 | docker-compose.yml 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["apps/*", "libs/*"] 3 | 4 | [profile.release] 5 | strip = true # Automatically strip symbols from the binary. 6 | opt-level = "z" # Optimize for size. 7 | lto = true # Link time optimization. 8 | codegen-units = 1 # Allow for maximum size reduction optimizations. 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.65.0 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY . . 6 | 7 | RUN cargo build --release --bin discuz-server 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 ken.yip 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RUN_MODE = "testing" 3 | CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true 4 | 5 | [tasks.pre-commit] 6 | workspace = false 7 | dependencies = ["lint", "spell", "format", "test"] 8 | 9 | [tasks.lint] 10 | workspace = false 11 | command = "cargo" 12 | args = ["clippy", "--tests", "--examples", "--all-targets", "--workspace"] 13 | 14 | [tasks.lint-fix] 15 | args = ["clippy", "--fix", "--allow-dirty"] 16 | 17 | [tasks.format] 18 | workspace = false 19 | command = "cargo" 20 | args = ["fmt", "--all"] 21 | 22 | [tasks.test] 23 | env = { RUN_MODE = "testing", RUST_BACKTRACE = "0", RUST_LOG = "discuz-server=trace,discuz-layers=trace" } 24 | command = "cargo" 25 | args = ["nextest", "run"] 26 | 27 | [tasks.cargo-test] 28 | env = { RUN_MODE = "testing", RUST_BACKTRACE = "0", RUST_LOG = "discuz-server=trace,discuz-layers=trace" } 29 | command = "cargo" 30 | args = ["test"] 31 | 32 | [tasks.spell] 33 | workspace = false 34 | command = "cspell" 35 | args = ["**"] 36 | 37 | [tasks.dev] 38 | workspace = false 39 | cwd = "./apps/server" 40 | env = { RUN_MODE = "development", RUST_BACKTRACE = "0", TRACING_LOG = "discuz-server=trace,discuz-layers=trace" } 41 | command = "cargo" 42 | args = ["watch", "-x run"] 43 | 44 | [tasks.db-migrate] 45 | workspace = false 46 | cwd = "./apps/db-migration" 47 | command = "cargo" 48 | args = ["run"] 49 | 50 | [tasks.cov] 51 | workspace = false 52 | command = "cargo" 53 | env = { RUN_MODE = "testing" } 54 | args = ["llvm-cov", "nextest", "${@}"] 55 | 56 | [tasks.cov-ci] 57 | workspace = false 58 | command = "cargo" 59 | env = { RUN_MODE = "ci" } 60 | args = ["llvm-cov", "nextest", "--lcov", "--output-path", "lcov.info"] 61 | 62 | [tasks.cov-check] 63 | command = "curl" 64 | workspace = false 65 | description = "Validate the repository codecov YAML" 66 | args = ["--data-binary", "@codecov.yml", "https://codecov.io/validate"] 67 | -------------------------------------------------------------------------------- /apps/db-migration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "db-migration" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Ken "] 6 | license = "ISC" 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | discuz-core = { path = "../../libs/core" } 11 | discuz-utils = { path = "../../libs/utils" } 12 | dotenv = "0.15.0" 13 | sea-orm = { version = "0.10.5", features = [ 14 | "sqlx-mysql", 15 | "runtime-tokio-native-tls", 16 | ] } 17 | tokio = { version = "1", features = ["full"] } 18 | -------------------------------------------------------------------------------- /apps/db-migration/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod utils; 2 | -------------------------------------------------------------------------------- /apps/db-migration/src/main.rs: -------------------------------------------------------------------------------- 1 | use db_migration::utils::database::get_db_connection; 2 | use discuz_core::migration::{Migrator, MigratorTrait}; 3 | use dotenv::dotenv; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<(), ()> { 7 | dotenv().ok(); 8 | let db_connection = get_db_connection().await; 9 | // Apply all the database migrations 10 | Migrator::up(&db_connection, None) 11 | .await 12 | .expect("Unable to migrate the database"); 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /apps/db-migration/src/utils/database.rs: -------------------------------------------------------------------------------- 1 | use discuz_utils::config::{get_config, Config, Database as DatabaseConfig}; 2 | use sea_orm::*; 3 | 4 | pub async fn get_db_connection() -> DatabaseConnection { 5 | // Get the database connection information 6 | let config = get_config(); 7 | let Config { 8 | database: database_config, 9 | .. 10 | } = config; 11 | 12 | let DatabaseConfig { 13 | username, 14 | password, 15 | port, 16 | hostname, 17 | database, 18 | .. 19 | } = database_config; 20 | let database_url = format!("mysql://{username}:{password}@{hostname}:{port}"); 21 | let database_name = database.to_owned(); 22 | 23 | let db_connection = Database::connect(&database_url) 24 | .await 25 | .expect("Unable to create database connection"); 26 | 27 | // Create the database if it isn't created 28 | db_connection 29 | .execute(Statement::from_string( 30 | DbBackend::MySql, 31 | format!("CREATE DATABASE IF NOT EXISTS `{database_name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"), 32 | )) 33 | .await 34 | .expect("Unable to create database"); 35 | 36 | Database::connect(format!("{database_url}/{database_name}")) 37 | .await 38 | .expect("Unable to create database connection to the database") 39 | } 40 | -------------------------------------------------------------------------------- /apps/db-migration/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod database; 2 | -------------------------------------------------------------------------------- /apps/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "discuz-server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Ken "] 6 | license = "ISC" 7 | 8 | [dependencies] 9 | actix-cors = "0.6.4" 10 | actix-web = "4.2.1" 11 | anyhow = "1.0.68" 12 | async-trait = "0.1.60" 13 | aws-config = "0.52.0" 14 | chrono = "0.4.23" 15 | derive_more = "0.99.17" 16 | discuz-core = { path = "../../libs/core" } 17 | discuz-utils = { path = "../../libs/utils" } 18 | dotenv = "0.15.0" 19 | error-stack = "0.2.4" 20 | fred = "5.2.0" 21 | futures = "0.3.25" 22 | futures-util = "0.3.25" 23 | sea-orm = { version = "0.10.5", features = [ 24 | "sqlx-mysql", 25 | "sqlx-sqlite", 26 | "runtime-tokio-native-tls", 27 | ] } 28 | serde = { version = "1.0.150", features = ["derive"] } 29 | serde_json = "1.0.89" 30 | tokio = { version = "1", features = ["full"] } 31 | tracing = "0.1" 32 | tracing-subscriber = "0.3.16" 33 | uuid = "1.2.2" 34 | validator = "0.16.0" 35 | validator_derive = "0.16.0" 36 | -------------------------------------------------------------------------------- /apps/server/Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RUN_MODE = "testing" 3 | CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true 4 | 5 | [tasks.test] 6 | env = { RUN_MODE = "testing", RUST_BACKTRACE = "0", RUST_LOG = "discuz-server=trace,discuz-core=trace" } 7 | command = "cargo" 8 | args = ["test", "--","--show-output"] 9 | 10 | [tasks.nextest] 11 | env = { RUN_MODE = "testing", RUST_BACKTRACE = "0", RUST_LOG = "discuz-server=trace,discuz-core=trace" } 12 | command = "cargo" 13 | args = ["nextest", "run", "--no-capture"] 14 | -------------------------------------------------------------------------------- /apps/server/src/auth/auth_controller/get_tokens.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpResponse, Result}; 2 | use discuz_core::service::auth::{ 3 | auth_service::AuthServiceTrait, provider::api_provider::GetTokensOutput, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | use tracing::trace; 7 | 8 | use crate::{auth::errors::ApiAuthError, errors::AppError, utils::app_state::AppState}; 9 | 10 | pub async fn execute( 11 | app_state: web::Data, 12 | body: web::Json, 13 | ) -> Result { 14 | let tokens = app_state 15 | .auth_service 16 | .get_tokens(&body.code) 17 | .await 18 | .map_err(|error| { 19 | trace!("{:#?}", error); 20 | ApiAuthError::InvalidAuthCode 21 | })?; 22 | Ok(HttpResponse::Ok().json(Response { data: tokens })) 23 | } 24 | 25 | #[derive(Debug, Serialize)] 26 | pub struct Response { 27 | pub data: GetTokensOutput, 28 | } 29 | 30 | #[derive(Debug, Deserialize)] 31 | pub struct Body { 32 | pub code: String, 33 | } 34 | -------------------------------------------------------------------------------- /apps/server/src/auth/auth_controller/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_tokens; 2 | -------------------------------------------------------------------------------- /apps/server/src/auth/auth_route.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web; 2 | 3 | use crate::auth::auth_controller::get_tokens; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service(web::resource("/code").route(web::post().to(get_tokens::execute))); 7 | } 8 | -------------------------------------------------------------------------------- /apps/server/src/auth/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{ErrorDetail, GetErrorDetailTrait}; 2 | use discuz_core::service::auth::errors::AuthError; 3 | 4 | #[derive(Debug, Clone)] 5 | pub enum ApiAuthError { 6 | Generic, 7 | UserBanned, 8 | InvalidAuthCode, 9 | InsufficientPrivilege, 10 | MissingAuthorization, 11 | InternalServerError, 12 | } 13 | 14 | impl GetErrorDetailTrait for ApiAuthError { 15 | fn get_error_detail(&self) -> ErrorDetail { 16 | match self { 17 | ApiAuthError::Generic => ErrorDetail { 18 | code: "auth_generic".to_owned(), 19 | status: 400, 20 | message: None, 21 | detail: None, 22 | }, 23 | ApiAuthError::InvalidAuthCode => ErrorDetail { 24 | code: "auth_invalid_auth_token".to_owned(), 25 | status: 400, 26 | message: None, 27 | detail: None, 28 | }, 29 | ApiAuthError::InsufficientPrivilege => ErrorDetail { 30 | code: "auth_insufficient_privilege".to_owned(), 31 | status: 403, 32 | message: None, 33 | detail: None, 34 | }, 35 | ApiAuthError::UserBanned => ErrorDetail { 36 | code: "auth_user_banned".to_owned(), 37 | status: 403, 38 | message: None, 39 | detail: None, 40 | }, 41 | ApiAuthError::MissingAuthorization => ErrorDetail { 42 | code: "auth_missing_authorization".to_owned(), 43 | status: 401, 44 | message: None, 45 | detail: None, 46 | }, 47 | ApiAuthError::InternalServerError => ErrorDetail { 48 | code: "auth_internal_server_error".to_owned(), 49 | status: 500, 50 | message: None, 51 | detail: None, 52 | }, 53 | } 54 | } 55 | } 56 | 57 | impl From for ApiAuthError { 58 | fn from(auth_error: AuthError) -> Self { 59 | match auth_error { 60 | AuthError::Generic(_) => ApiAuthError::Generic, 61 | AuthError::InvalidAccessTokenError => ApiAuthError::Generic, 62 | AuthError::InsufficientPrivilegesError => ApiAuthError::InsufficientPrivilege, 63 | AuthError::UserBannedError { 64 | reason: _, 65 | ban_time: _, 66 | release_time: _, 67 | } => ApiAuthError::UserBanned, 68 | AuthError::InternalServerError => ApiAuthError::InternalServerError, 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /apps/server/src/auth/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth_controller; 2 | pub mod auth_route; 3 | pub mod errors; 4 | -------------------------------------------------------------------------------- /apps/server/src/category/category_controller/create.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpResponse, Result}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use discuz_core::service::{ 5 | auth::{auth_service::AuthServiceTrait, constants::UserRole}, 6 | category::category_service::{Category, CategoryServiceTrait, CreateCategoryInput}, 7 | }; 8 | 9 | use crate::{ 10 | auth::errors::ApiAuthError, category::errors::ApiCategoryError, errors::AppError, 11 | utils::app_state::AppState, utils::auth::Auth, 12 | }; 13 | 14 | pub async fn execute( 15 | app_state: web::Data, 16 | body: web::Json, 17 | auth: Auth, 18 | ) -> Result { 19 | let auth_service = app_state.auth_service.clone(); 20 | let user = auth.user.ok_or(ApiAuthError::MissingAuthorization)?; 21 | 22 | auth_service 23 | .validate_user(&user, Some(&[UserRole::Admin])) 24 | .map_err(|_| ApiAuthError::InsufficientPrivilege)?; 25 | 26 | let input = CreateCategoryInput { 27 | name: body.name.to_owned(), 28 | description: body.description.to_owned(), 29 | postable: body.postable.unwrap_or(false), 30 | level: body.level.unwrap_or(0), 31 | sort_index: None, 32 | user_id: Some(user.id.to_owned()), 33 | parent_id: body.parent_id.to_owned(), 34 | }; 35 | 36 | let category_service = app_state.category_service.clone(); 37 | 38 | let category = category_service 39 | .create(&input) 40 | .await 41 | .map_err(|_| ApiCategoryError::InternalSeverError)?; 42 | 43 | Ok(HttpResponse::Ok().json(Response { data: category })) 44 | } 45 | 46 | #[derive(Debug, Serialize, Deserialize)] 47 | pub struct Response { 48 | pub data: Category, 49 | } 50 | 51 | #[derive(Debug, Clone, Serialize, Deserialize)] 52 | pub struct Body { 53 | pub name: String, 54 | pub level: Option, 55 | pub postable: Option, 56 | pub description: Option, 57 | pub parent_id: Option, 58 | } 59 | -------------------------------------------------------------------------------- /apps/server/src/category/category_controller/list.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpResponse, Result}; 2 | use tracing::error; 3 | 4 | use discuz_core::service::{auth::constants::UserRole, prelude::*}; 5 | 6 | use crate::{ 7 | auth::errors::ApiAuthError, category::errors::ApiCategoryError, errors::AppError, 8 | utils::app_state::AppState, utils::auth::Auth, 9 | }; 10 | 11 | pub async fn execute(app_state: web::Data, auth: Auth) -> Result { 12 | let auth_service = app_state.auth_service.clone(); 13 | let user = auth.user.ok_or(ApiAuthError::MissingAuthorization)?; 14 | 15 | auth_service 16 | .validate_user(&user, Some(&[UserRole::Admin])) 17 | .map_err(|_| ApiAuthError::InsufficientPrivilege)?; 18 | 19 | let category_service = app_state.category_service.clone(); 20 | 21 | let response = category_service 22 | .get_categories(None) 23 | .await 24 | .map_err(|error| { 25 | error!("{:#?}", error); 26 | ApiCategoryError::InternalSeverError 27 | })?; 28 | 29 | Ok(HttpResponse::Ok().json(response)) 30 | } 31 | 32 | pub type Response = GetCategoriesResponse; 33 | -------------------------------------------------------------------------------- /apps/server/src/category/category_controller/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create; 2 | pub mod list; 3 | -------------------------------------------------------------------------------- /apps/server/src/category/category_route.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web; 2 | 3 | use crate::category::category_controller; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::resource("") 8 | .route(web::get().to(category_controller::list::execute)) 9 | .route(web::post().to(category_controller::create::execute)), 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/server/src/category/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{ErrorDetail, GetErrorDetailTrait}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub enum ApiCategoryError { 5 | InternalSeverError, 6 | } 7 | 8 | impl GetErrorDetailTrait for ApiCategoryError { 9 | fn get_error_detail(&self) -> ErrorDetail { 10 | match self { 11 | ApiCategoryError::InternalSeverError => ErrorDetail { 12 | code: "post_internal_server_error".to_owned(), 13 | status: 500, 14 | message: Some("Internal Server Error".to_owned()), 15 | detail: None, 16 | }, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/server/src/category/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod category_controller; 2 | pub mod category_route; 3 | pub mod errors; 4 | -------------------------------------------------------------------------------- /apps/server/src/config/config_controller/get_config.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpResponse, Result}; 2 | use discuz_core::service::prelude::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{config::errors::ApiConfigError, errors::AppError, utils::app_state::AppState}; 6 | 7 | pub async fn execute(app_state: web::Data) -> Result { 8 | let config_service = app_state.config_service.clone(); 9 | let data = config_service 10 | .get_config() 11 | .await 12 | .map_err(|_| ApiConfigError::InternalSeverError)?; 13 | Ok(HttpResponse::Ok().json(Response { data })) 14 | } 15 | 16 | #[derive(Serialize, Deserialize)] 17 | pub struct Response { 18 | pub data: AppConfig, 19 | } 20 | -------------------------------------------------------------------------------- /apps/server/src/config/config_controller/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_config; 2 | -------------------------------------------------------------------------------- /apps/server/src/config/config_route.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web; 2 | 3 | use crate::config::config_controller; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service(web::resource("").route(web::get().to(config_controller::get_config::execute))); 7 | } 8 | -------------------------------------------------------------------------------- /apps/server/src/config/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{ErrorDetail, GetErrorDetailTrait}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub enum ApiConfigError { 5 | InternalSeverError, 6 | } 7 | 8 | impl GetErrorDetailTrait for ApiConfigError { 9 | fn get_error_detail(&self) -> ErrorDetail { 10 | match self { 11 | ApiConfigError::InternalSeverError => ErrorDetail { 12 | code: "config_internal_server_error".to_owned(), 13 | status: 500, 14 | message: Some("Internal Server Error".to_owned()), 15 | detail: None, 16 | }, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/server/src/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config_controller; 2 | pub mod config_route; 3 | pub mod errors; 4 | -------------------------------------------------------------------------------- /apps/server/src/errors.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{error::ResponseError, http::StatusCode, HttpResponse}; 2 | use serde::Serialize; 3 | use std::{convert::From, fmt}; 4 | use uuid::Uuid; 5 | 6 | use crate::{ 7 | auth::errors::ApiAuthError, category::errors::ApiCategoryError, config::errors::ApiConfigError, 8 | post::errors::ApiPostError, 9 | }; 10 | 11 | #[derive(Debug, Serialize, Clone)] 12 | pub struct AppError { 13 | // Used for tracing the error in log monitor software 14 | pub id: String, 15 | // 16 | pub code: String, 17 | // Conveying the HTTP status code 18 | pub status: u16, 19 | // A short, human-readable message for the general error type; the title should not change for given types 20 | pub message: Option, 21 | // A human-readable description of the specific error 22 | pub detail: Option, 23 | } 24 | 25 | #[derive(Debug, Serialize, Clone)] 26 | struct Response { 27 | error: AppError, 28 | } 29 | 30 | pub struct ErrorDetail { 31 | // 32 | pub code: String, 33 | // Conveying the HTTP status code 34 | pub status: u16, 35 | // A short, human-readable message for the general error type; the title should not change for given types 36 | pub message: Option, 37 | // A human-readable description of the specific error 38 | pub detail: Option, 39 | } 40 | 41 | pub trait GetErrorDetailTrait { 42 | fn get_error_detail(&self) -> ErrorDetail; 43 | } 44 | 45 | impl AppError { 46 | pub fn internal_server_error() -> AppError { 47 | AppError { 48 | id: Uuid::new_v4().to_string(), 49 | code: "app_internal_server_error".to_string(), 50 | status: 500, 51 | message: Some("Internal Server Error".to_owned()), 52 | detail: Some( 53 | "The server encountered an unexpected condition which prevented it from fulfilling the request.".to_owned() 54 | ), 55 | } 56 | } 57 | } 58 | 59 | impl From for AppError { 60 | fn from(error: ApiAuthError) -> Self { 61 | let detail = error.get_error_detail(); 62 | AppError { 63 | id: Uuid::new_v4().to_string(), 64 | code: detail.code, 65 | status: detail.status, 66 | message: detail.message, 67 | detail: detail.detail, 68 | } 69 | } 70 | } 71 | 72 | impl From for AppError { 73 | fn from(error: ApiPostError) -> Self { 74 | let detail = error.get_error_detail(); 75 | AppError { 76 | id: Uuid::new_v4().to_string(), 77 | code: detail.code, 78 | status: detail.status, 79 | message: detail.message, 80 | detail: detail.detail, 81 | } 82 | } 83 | } 84 | 85 | impl From for AppError { 86 | fn from(error: ApiCategoryError) -> Self { 87 | let detail = error.get_error_detail(); 88 | AppError { 89 | id: Uuid::new_v4().to_string(), 90 | code: detail.code, 91 | status: detail.status, 92 | message: detail.message, 93 | detail: detail.detail, 94 | } 95 | } 96 | } 97 | 98 | impl From for AppError { 99 | fn from(error: ApiConfigError) -> Self { 100 | let detail = error.get_error_detail(); 101 | AppError { 102 | id: Uuid::new_v4().to_string(), 103 | code: detail.code, 104 | status: detail.status, 105 | message: detail.message, 106 | detail: detail.detail, 107 | } 108 | } 109 | } 110 | 111 | impl fmt::Display for AppError { 112 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 113 | write!(f, "{self:#?}") 114 | } 115 | } 116 | 117 | impl ResponseError for AppError { 118 | fn error_response(&self) -> HttpResponse { 119 | HttpResponse::build(StatusCode::from_u16(self.status).unwrap()).json(Response { 120 | error: self.to_owned(), 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /apps/server/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod category; 3 | pub mod config; 4 | pub mod post; 5 | pub mod user; 6 | 7 | pub mod app; 8 | pub mod errors; 9 | pub mod utils; 10 | -------------------------------------------------------------------------------- /apps/server/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate tracing; 3 | 4 | use dotenv::dotenv; 5 | use std::env; 6 | use tracing_subscriber::fmt::Subscriber; 7 | use tracing_subscriber::EnvFilter; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | dotenv().ok(); 12 | 13 | // Set the default value of RUST_LOG if it isn't explicitly defined 14 | if env::var("RUST_LOG").is_err() { 15 | env::set_var("RUST_LOG", "debug"); 16 | } 17 | 18 | // Set the default value of TRACING_LOG if it isn't explicitly defined 19 | if env::var("TRACING_LOG").is_err() { 20 | env::set_var("TRACING_LOG", "discuz-core=debug"); 21 | } 22 | 23 | let subscriber = Subscriber::builder() 24 | .with_env_filter(EnvFilter::from_default_env()) 25 | .finish(); 26 | 27 | // Set up the default logging subscriber for the logging. 28 | // Allow the logging to print on the screen 29 | tracing::subscriber::set_global_default(subscriber).expect("Setting default subscriber failed"); 30 | 31 | let result = discuz_server::app::listen().await; 32 | if let Some(error) = result.err() { 33 | error!("{:#?}", error); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/server/src/post/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{ErrorDetail, GetErrorDetailTrait}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub enum ApiPostError { 5 | Generic, 6 | InvalidRequest { detail: Option }, 7 | InternalServerError, 8 | } 9 | 10 | impl GetErrorDetailTrait for ApiPostError { 11 | fn get_error_detail(&self) -> ErrorDetail { 12 | match self { 13 | ApiPostError::Generic => ErrorDetail { 14 | code: "post_generic".to_owned(), 15 | status: 400, 16 | message: None, 17 | detail: None, 18 | }, 19 | ApiPostError::InvalidRequest { detail } => ErrorDetail { 20 | code: "post_invalid_post_request".to_owned(), 21 | status: 400, 22 | message: Some("Invalid Request".to_owned()), 23 | detail: detail.to_owned(), 24 | }, 25 | ApiPostError::InternalServerError => ErrorDetail { 26 | code: "post_internal_server_error".to_owned(), 27 | status: 500, 28 | message: Some("Internal Server Error".to_owned()), 29 | detail: None, 30 | }, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/server/src/post/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | pub mod post_controller; 3 | pub mod post_route; 4 | -------------------------------------------------------------------------------- /apps/server/src/post/post_controller/create.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpResponse, Result}; 2 | use serde::{Deserialize, Serialize}; 3 | use tracing::trace; 4 | 5 | use discuz_core::service::{ 6 | post::post_service::{CreatePostInput, Post, PostServiceTrait}, 7 | prelude::AuthServiceTrait, 8 | }; 9 | 10 | use crate::{ 11 | auth::errors::ApiAuthError, errors::AppError, post::errors::ApiPostError, 12 | utils::app_state::AppState, utils::auth::Auth, 13 | }; 14 | 15 | pub async fn execute( 16 | app_state: web::Data, 17 | auth: Auth, 18 | body: web::Json, 19 | ) -> Result { 20 | let auth_service = app_state.auth_service.clone(); 21 | 22 | let user = auth.user.ok_or(ApiAuthError::MissingAuthorization)?; 23 | 24 | auth_service.validate_user(&user, None).map_err(|error| { 25 | trace!("{:#?}", error); 26 | ApiAuthError::UserBanned 27 | })?; 28 | 29 | let post_service = app_state.post_service.clone(); 30 | 31 | let input = CreatePostInput { 32 | title: body.title.to_owned(), 33 | category_id: body.category_id.to_owned(), 34 | content: body.content.to_owned(), 35 | user_id: user.id, 36 | }; 37 | 38 | let post = post_service.create(&input).await.map_err(|error| { 39 | trace!("{:#?}", error); 40 | ApiPostError::InternalServerError 41 | })?; 42 | 43 | Ok(HttpResponse::Ok().json(Response { data: post })) 44 | } 45 | 46 | #[derive(Debug, Serialize, Deserialize)] 47 | pub struct Response { 48 | pub data: Post, 49 | } 50 | 51 | #[derive(Debug, Clone, Serialize, Deserialize)] 52 | pub struct Body { 53 | pub title: String, 54 | pub category_id: String, 55 | pub content: String, 56 | } 57 | -------------------------------------------------------------------------------- /apps/server/src/post/post_controller/delete.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpResponse, Result}; 2 | use serde::{Deserialize, Serialize}; 3 | use tracing::trace; 4 | 5 | use discuz_core::service::{ 6 | auth::{auth_service::AuthServiceTrait, constants::UserRole}, 7 | post::{errors::PostError, post_service::PostServiceTrait}, 8 | }; 9 | 10 | use crate::{ 11 | auth::errors::ApiAuthError, errors::AppError, post::errors::ApiPostError, 12 | utils::app_state::AppState, utils::auth::Auth, 13 | }; 14 | 15 | pub async fn execute( 16 | auth: Auth, 17 | app_state: web::Data, 18 | params: web::Path, 19 | ) -> Result { 20 | let auth_service = app_state.auth_service.clone(); 21 | let user = auth.user.ok_or(ApiAuthError::MissingAuthorization)?; 22 | 23 | auth_service.validate_user(&user, None).map_err(|error| { 24 | trace!("{:#?}", error); 25 | ApiAuthError::UserBanned 26 | })?; 27 | 28 | auth_service 29 | .validate_user(&user, Some(&[UserRole::Admin])) 30 | .map_err(|_| ApiAuthError::InsufficientPrivilege)?; 31 | 32 | let post_service = app_state.post_service.clone(); 33 | 34 | post_service.delete(params.id).await.map_err(|error| { 35 | trace!("{:#?}", error); 36 | match error.current_context() { 37 | PostError::PostNotExistError => ApiPostError::InvalidRequest { 38 | detail: Some(format!("Id #{} is not a valid post id", params.id)), 39 | }, 40 | _ => ApiPostError::InternalServerError, 41 | } 42 | })?; 43 | 44 | Ok(HttpResponse::Ok().finish()) 45 | } 46 | 47 | #[derive(Debug, Serialize, Deserialize)] 48 | pub struct Params { 49 | pub id: i32, 50 | } 51 | -------------------------------------------------------------------------------- /apps/server/src/post/post_controller/get.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpResponse, Result}; 2 | use serde::{Deserialize, Serialize}; 3 | use tracing::trace; 4 | 5 | use discuz_core::service::post::post_service::{Post, PostServiceTrait}; 6 | 7 | use crate::{errors::AppError, post::errors::ApiPostError, utils::app_state::AppState}; 8 | 9 | pub async fn execute( 10 | app_state: web::Data, 11 | params: web::Path, 12 | ) -> Result { 13 | let post_service = app_state.post_service.clone(); 14 | 15 | let post = post_service.find_by_id(params.id).await.map_err(|error| { 16 | trace!("{:#?}", error); 17 | ApiPostError::InternalServerError 18 | })?; 19 | 20 | if let Some(post) = post { 21 | if post.status_id == *"A" { 22 | return Ok(HttpResponse::Ok().json(Response { data: post })); 23 | } 24 | } 25 | 26 | Ok(HttpResponse::NotFound().finish()) 27 | } 28 | 29 | #[derive(Debug, Serialize, Deserialize)] 30 | pub struct Params { 31 | pub id: i32, 32 | } 33 | 34 | #[derive(Debug, Serialize, Deserialize)] 35 | pub struct Response { 36 | pub data: Post, 37 | } 38 | -------------------------------------------------------------------------------- /apps/server/src/post/post_controller/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create; 2 | pub mod delete; 3 | pub mod get; 4 | pub mod update; 5 | -------------------------------------------------------------------------------- /apps/server/src/post/post_controller/update.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpResponse, Result}; 2 | use serde::{Deserialize, Serialize}; 3 | use tracing::trace; 4 | 5 | use discuz_core::service::{ 6 | auth::{auth_service::AuthServiceTrait, constants::UserRole}, 7 | post::{ 8 | errors::PostError, 9 | post_service::{Post, PostServiceTrait, UpdatePostInput}, 10 | }, 11 | }; 12 | 13 | use crate::{ 14 | auth::errors::ApiAuthError, errors::AppError, post::errors::ApiPostError, 15 | utils::app_state::AppState, utils::auth::Auth, 16 | }; 17 | 18 | pub async fn execute( 19 | app_state: web::Data, 20 | auth: Auth, 21 | body: web::Json, 22 | params: web::Path, 23 | ) -> Result { 24 | let user = auth.user.ok_or(ApiAuthError::MissingAuthorization)?; 25 | let auth_service = app_state.auth_service.clone(); 26 | 27 | auth_service 28 | .validate_user(&user, Some(&[UserRole::Admin])) 29 | .map_err(|_| ApiAuthError::InsufficientPrivilege)?; 30 | 31 | let post_service = app_state.post_service.clone(); 32 | 33 | let input = UpdatePostInput { 34 | id: params.id.to_owned(), 35 | title: body.title.to_owned(), 36 | category_id: body.category_id.to_owned(), 37 | max_comment_count: None, 38 | content: body.content.to_owned(), 39 | status_id: None, 40 | }; 41 | 42 | let post = post_service.update(&input).await.map_err(|error| { 43 | trace!("{:#?}", error); 44 | match error.current_context() { 45 | PostError::PostNotExistError => ApiPostError::InvalidRequest { 46 | detail: Some(format!("Id #{} is not a valid post id", params.id)), 47 | }, 48 | _ => ApiPostError::InternalServerError, 49 | } 50 | })?; 51 | 52 | Ok(HttpResponse::Ok().json(Response { data: post })) 53 | } 54 | 55 | #[derive(Debug, Serialize, Deserialize)] 56 | pub struct Response { 57 | pub data: Post, 58 | } 59 | 60 | #[derive(Debug, Serialize, Deserialize)] 61 | pub struct Params { 62 | pub id: i32, 63 | } 64 | 65 | #[derive(Debug, Clone, Serialize, Deserialize)] 66 | pub struct Body { 67 | pub title: String, 68 | pub category_id: String, 69 | pub content: String, 70 | } 71 | -------------------------------------------------------------------------------- /apps/server/src/post/post_route.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web; 2 | 3 | use crate::post::post_controller; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service(web::resource("").route(web::post().to(post_controller::create::execute))); 7 | cfg.service( 8 | web::resource("/{id}") 9 | .route(web::get().to(post_controller::get::execute)) 10 | .route(web::patch().to(post_controller::update::execute)) 11 | .route(web::delete().to(post_controller::delete::execute)), 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/server/src/user/dto/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user; 2 | -------------------------------------------------------------------------------- /apps/server/src/user/dto/user.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use discuz_core::repository::repo_user::User; 3 | use serde::{Deserialize, Serialize}; 4 | use std::convert::From; 5 | 6 | #[derive(Debug, Serialize, Deserialize)] 7 | pub struct DtoUser { 8 | pub id: String, 9 | pub name: String, 10 | pub email: String, 11 | pub avatar_url: Option, 12 | pub created_at: DateTime, 13 | pub updated_at: DateTime, 14 | } 15 | 16 | impl From for DtoUser { 17 | fn from(user: User) -> Self { 18 | Self { 19 | id: user.id, 20 | name: user.name, 21 | email: user.email, 22 | avatar_url: user.avatar_url, 23 | created_at: user.created_at, 24 | updated_at: user.updated_at, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/server/src/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dto; 2 | pub mod user_controller; 3 | pub mod user_route; 4 | -------------------------------------------------------------------------------- /apps/server/src/user/user_controller/get_me.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpResponse, Result}; 2 | use discuz_core::service::prelude::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{ 6 | auth::errors::ApiAuthError, errors::AppError, user::dto::user::DtoUser, 7 | utils::app_state::AppState, utils::auth::Auth, 8 | }; 9 | 10 | pub async fn execute(app_state: web::Data, auth: Auth) -> Result { 11 | let user = auth.user.ok_or(ApiAuthError::MissingAuthorization)?; 12 | 13 | let auth_service = app_state.auth_service.clone(); 14 | auth_service.validate_user(&user, None).map_err(|error| { 15 | let error = error.current_context().to_owned(); 16 | ApiAuthError::from(error) 17 | })?; 18 | 19 | Ok(HttpResponse::Ok().json(Response { data: user.into() })) 20 | } 21 | 22 | #[derive(Debug, Serialize, Deserialize)] 23 | pub struct Response { 24 | pub data: DtoUser, 25 | } 26 | -------------------------------------------------------------------------------- /apps/server/src/user/user_controller/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_me; 2 | pub mod update_user; 3 | -------------------------------------------------------------------------------- /apps/server/src/user/user_controller/update_user.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpResponse}; 2 | use discuz_core::service::user::user_service::{UpdateUserInput, UserServiceTrait}; 3 | use serde::{Deserialize, Serialize}; 4 | use tracing::trace; 5 | 6 | use crate::{ 7 | auth::errors::ApiAuthError, 8 | errors::AppError, 9 | user::dto::user::DtoUser, 10 | utils::{app_state::AppState, auth::Auth}, 11 | }; 12 | 13 | pub async fn execute( 14 | params: web::Path, 15 | body: web::Json, 16 | app_state: web::Data, 17 | auth: Auth, 18 | ) -> Result { 19 | let user = auth.user.ok_or(ApiAuthError::MissingAuthorization)?; 20 | let id = params.id.to_owned(); 21 | 22 | if user.id != id { 23 | return Err(ApiAuthError::InsufficientPrivilege.into()); 24 | } 25 | 26 | let updates = UpdateUserInput { 27 | id: id.to_owned(), 28 | name: body.name.to_owned(), 29 | avatar_url: body.avatar_url.to_owned(), 30 | }; 31 | 32 | let user_service = app_state.user_service.clone(); 33 | 34 | user_service.update(&updates).await.map_err(|error| { 35 | trace!("{:#?}", error); 36 | AppError::internal_server_error() 37 | })?; 38 | 39 | let user = user_service 40 | .find_by_id(&id) 41 | .await 42 | .map_err(|error| { 43 | trace!("{:#?}", error); 44 | AppError::internal_server_error() 45 | })? 46 | .ok_or_else(AppError::internal_server_error)?; 47 | 48 | Ok(HttpResponse::Ok().json(Response { data: user.into() })) 49 | } 50 | 51 | #[derive(Debug, Deserialize)] 52 | pub struct Params { 53 | pub id: String, 54 | } 55 | 56 | #[derive(Debug, Serialize, Deserialize)] 57 | pub struct Body { 58 | pub name: String, 59 | pub avatar_url: Option, 60 | } 61 | 62 | #[derive(Debug, Serialize, Deserialize)] 63 | pub struct Response { 64 | pub data: DtoUser, 65 | } 66 | -------------------------------------------------------------------------------- /apps/server/src/user/user_route.rs: -------------------------------------------------------------------------------- 1 | use actix_web::web; 2 | 3 | use crate::user::user_controller; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service(web::resource("/me").route(web::get().to(user_controller::get_me::execute))); 7 | cfg.service( 8 | web::resource("/{id}").route(web::patch().to(user_controller::update_user::execute)), 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/server/src/utils/app_state.rs: -------------------------------------------------------------------------------- 1 | use aws_config::SdkConfig; 2 | use discuz_core::service::prelude::*; 3 | use fred::clients::RedisClient; 4 | use sea_orm::DatabaseConnection; 5 | use std::sync::Arc; 6 | 7 | #[derive(Clone)] 8 | pub struct AppState { 9 | pub db_connection: Arc, 10 | pub sdk_config: Arc, 11 | pub redis_client: Arc>, 12 | pub auth_service: Arc, 13 | pub config_service: Arc, 14 | pub file_service: Arc, 15 | pub post_service: Arc, 16 | pub category_service: Arc, 17 | pub user_service: Arc, 18 | } 19 | -------------------------------------------------------------------------------- /apps/server/src/utils/auth.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | 3 | use actix_web::{dev, Error, FromRequest, HttpRequest, Result}; 4 | use discuz_core::service::user::user_service::{User, UserServiceTrait}; 5 | use futures::Future; 6 | use serde::Deserialize; 7 | use tracing::trace; 8 | 9 | use crate::utils::app_state::AppState; 10 | 11 | #[derive(Deserialize)] 12 | pub struct Auth { 13 | pub user: Option, 14 | } 15 | 16 | impl FromRequest for Auth { 17 | type Error = Error; 18 | type Future = Pin>>>; 19 | fn from_request(req: &HttpRequest, _: &mut dev::Payload) -> Self::Future { 20 | let access_token = Self::extract_access_token(req); 21 | let user_service = req 22 | .app_data::>() 23 | .expect("The user service is not set up in the app state") 24 | .user_service 25 | .clone(); 26 | Box::pin(async move { 27 | if let Some(access_token) = access_token { 28 | match user_service.get_profile(&access_token).await { 29 | Ok(user) => Ok(Auth { user: Some(user) }), 30 | Err(error) => { 31 | trace!("Unable to create profile by the access token\n{:#?}", error); 32 | Ok(Auth { user: None }) 33 | } 34 | } 35 | } else { 36 | trace!("Unable to extract access token from the header"); 37 | Ok(Auth { user: None }) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | impl Auth { 44 | fn extract_access_token(req: &HttpRequest) -> Option { 45 | if let Some(access_token) = req.headers().get("authorization") { 46 | let access_token = if let Ok(access_token) = access_token.to_str() { 47 | access_token.to_owned() 48 | } else { 49 | trace!("[auth.rs] Unable to convert the access token from header value to string"); 50 | return None; 51 | }; 52 | let mut chucks = access_token.split(' ').to_owned(); 53 | let policy = chucks.next(); 54 | if let Some(policy) = policy { 55 | if policy.to_lowercase() != "bearer" { 56 | return None; 57 | } 58 | } 59 | chucks.next().map(|access_token| access_token.to_owned()) 60 | } else { 61 | None 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /apps/server/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app_state; 2 | pub mod auth; 3 | -------------------------------------------------------------------------------- /apps/server/tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | mod integrations; 2 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/category/create.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::{ 3 | constants::FAKE_ACCESS_TOKEN, 4 | migration::{Migrator, MigratorTrait}, 5 | service::{auth::constants::UserRole, user::user_service::UserServiceTrait}, 6 | }; 7 | use discuz_server::{app, category::category_controller, user::user_controller}; 8 | 9 | #[actix_web::test] 10 | async fn create_category() { 11 | let app_state = app::get_app_state().await; 12 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 13 | let app = test::init_service( 14 | App::new() 15 | .app_data(web::Data::new(app_state.clone())) 16 | .service(web::scope("/api").configure(app::get_api_routes)), 17 | ) 18 | .await; 19 | 20 | let body = category_controller::create::Body { 21 | name: "New Category Id".to_owned(), 22 | postable: None, 23 | level: None, 24 | description: None, 25 | parent_id: None, 26 | }; 27 | 28 | // Get the user id 29 | let req = test::TestRequest::get() 30 | .uri("/api/user/me") 31 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 32 | .to_request(); 33 | let resp = test::call_service(&app, req).await; 34 | assert!(resp.status().is_success()); 35 | 36 | let user_body: user_controller::get_me::Response = test::read_body_json(resp).await; 37 | let user_id = user_body.data.id; 38 | 39 | // Update the user's role 40 | app_state 41 | .user_service 42 | .update_role(&user_id, &UserRole::Admin) 43 | .await 44 | .unwrap(); 45 | 46 | // Run test 47 | let req = test::TestRequest::post() 48 | .uri("/api/category") 49 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 50 | .set_json(body.to_owned()) 51 | .to_request(); 52 | 53 | let resp = test::call_service(&app, req).await; 54 | 55 | assert!(resp.status().is_success()); 56 | 57 | let create_category_resp: category_controller::create::Response = 58 | test::read_body_json(resp).await; 59 | 60 | assert_eq!(create_category_resp.data.name, body.name); 61 | assert_eq!(create_category_resp.data.description, body.description); 62 | } 63 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/category/create_error_401.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::migration::{Migrator, MigratorTrait}; 3 | use discuz_server::app; 4 | 5 | #[actix_web::test] 6 | async fn create_category_401() { 7 | let app_state = app::get_app_state().await; 8 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 9 | let app = test::init_service( 10 | App::new() 11 | .app_data(web::Data::new(app_state.clone())) 12 | .service(web::scope("/api").configure(app::get_api_routes)), 13 | ) 14 | .await; 15 | 16 | // It should reject with error code 401 17 | let req = test::TestRequest::post() 18 | .uri("/api/category") 19 | .append_header(("authorization", "bearer INVALID_ACCESS_TOKEN")) 20 | .to_request(); 21 | 22 | let resp = test::call_service(&app, req).await; 23 | 24 | assert!(resp.status().is_client_error()); 25 | assert_eq!(resp.status().as_u16(), 400); 26 | } 27 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/category/create_error_403.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::{ 3 | constants::FAKE_ACCESS_TOKEN, 4 | migration::{Migrator, MigratorTrait}, 5 | }; 6 | use discuz_server::{app, category::category_controller}; 7 | 8 | #[actix_web::test] 9 | async fn create_category() { 10 | let app_state = app::get_app_state().await; 11 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 12 | let app = test::init_service( 13 | App::new() 14 | .app_data(web::Data::new(app_state.clone())) 15 | .service(web::scope("/api").configure(app::get_api_routes)), 16 | ) 17 | .await; 18 | 19 | let body = category_controller::create::Body { 20 | name: "New Category Id".to_owned(), 21 | description: None, 22 | postable: None, 23 | level: None, 24 | parent_id: None, 25 | }; 26 | 27 | // It should reject with error code 403 28 | let req = test::TestRequest::post() 29 | .uri("/api/category") 30 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 31 | .set_json(body.to_owned()) 32 | .to_request(); 33 | 34 | let resp = test::call_service(&app, req).await; 35 | 36 | assert!(resp.status().is_client_error()); 37 | assert_eq!(resp.status().as_u16(), 403); 38 | } 39 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/category/list.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::{ 3 | constants::FAKE_ACCESS_TOKEN, 4 | migration::{Migrator, MigratorTrait}, 5 | service::{auth::constants::UserRole, user::user_service::UserServiceTrait}, 6 | }; 7 | use discuz_server::{app, category::category_controller::list::Response, user::user_controller}; 8 | 9 | #[actix_web::test] 10 | async fn list_category() { 11 | let app_state = app::get_app_state().await; 12 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 13 | let app = test::init_service( 14 | App::new() 15 | .app_data(web::Data::new(app_state.clone())) 16 | .service(web::scope("/api").configure(app::get_api_routes)), 17 | ) 18 | .await; 19 | 20 | // Get the user id 21 | let req = test::TestRequest::get() 22 | .uri("/api/user/me") 23 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 24 | .to_request(); 25 | let resp = test::call_service(&app, req).await; 26 | assert!(resp.status().is_success()); 27 | 28 | let user_body: user_controller::get_me::Response = test::read_body_json(resp).await; 29 | let user_id = user_body.data.id; 30 | 31 | // Update the user's role 32 | app_state 33 | .user_service 34 | .update_role(&user_id, &UserRole::Admin) 35 | .await 36 | .unwrap(); 37 | 38 | let req = test::TestRequest::get() 39 | .uri("/api/category") 40 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 41 | .to_request(); 42 | let resp = test::call_service(&app, req).await; 43 | assert!(resp.status().is_success()); 44 | 45 | let create_category_resp: Response = test::read_body_json(resp).await; 46 | 47 | assert!(!create_category_resp.data.is_empty()); 48 | assert!(create_category_resp.count > 0); 49 | } 50 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/category/list_403.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::migration::{Migrator, MigratorTrait}; 3 | use discuz_server::app; 4 | 5 | #[actix_web::test] 6 | async fn list_category_403() { 7 | let app_state = app::get_app_state().await; 8 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 9 | let app = test::init_service( 10 | App::new() 11 | .app_data(web::Data::new(app_state.clone())) 12 | .service(web::scope("/api").configure(app::get_api_routes)), 13 | ) 14 | .await; 15 | 16 | let req = test::TestRequest::get().uri("/api/category").to_request(); 17 | 18 | let resp = test::call_service(&app, req).await; 19 | assert!(resp.status().is_client_error()); 20 | assert_eq!(resp.status().as_u16(), 401); 21 | } 22 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/category/mod.rs: -------------------------------------------------------------------------------- 1 | mod create; 2 | mod create_error_401; 3 | mod create_error_403; 4 | mod list; 5 | mod list_403; 6 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/config/get_config.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::migration::{Migrator, MigratorTrait}; 3 | use discuz_server::{app, config::config_controller}; 4 | 5 | #[actix_web::test] 6 | async fn get_configs() { 7 | // 8 | // 9 | // Step (0): Set up the server 10 | // 11 | // 12 | let app_state = app::get_app_state().await; 13 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 14 | let app = test::init_service( 15 | App::new() 16 | .app_data(web::Data::new(app_state.clone())) 17 | .service(web::scope("/api").configure(app::get_api_routes)), 18 | ) 19 | .await; 20 | 21 | // 22 | // 23 | // Step (1): Sending the request 24 | // 25 | // 26 | let req = test::TestRequest::get().uri("/api/config").to_request(); 27 | let resp = test::call_service(&app, req).await; 28 | 29 | assert!(resp.status().is_success()); 30 | 31 | let create_post_resp: config_controller::get_config::Response = 32 | test::read_body_json(resp).await; 33 | assert_eq!(create_post_resp.data.app_status, "normal"); 34 | } 35 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_config; 2 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/health_check.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::migration::{Migrator, MigratorTrait}; 3 | use discuz_server::app; 4 | 5 | #[actix_web::test] 6 | async fn health_check() { 7 | let app_state = app::get_app_state().await; 8 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 9 | let app = test::init_service( 10 | App::new() 11 | .app_data(web::Data::new(app_state)) 12 | .service(web::scope("/api").configure(app::get_api_routes)), 13 | ) 14 | .await; 15 | let req = test::TestRequest::get() 16 | .uri("/api/health-check") 17 | .to_request(); 18 | let resp = test::call_service(&app, req).await; 19 | assert!(resp.status().is_success()); 20 | assert_eq!(resp.status().as_u16(), 200); 21 | } 22 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/mod.rs: -------------------------------------------------------------------------------- 1 | mod category; 2 | mod config; 3 | mod health_check; 4 | mod post; 5 | mod user; 6 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/post/create.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::{ 3 | constants::{FAKE_ACCESS_TOKEN, UNCLASSIFIED_CATEGORY_ID}, 4 | migration::{Migrator, MigratorTrait}, 5 | utils::mock_data, 6 | }; 7 | use discuz_server::{app, post::post_controller}; 8 | 9 | #[actix_web::test] 10 | async fn create_post() { 11 | // 12 | // 13 | // Step (0): Set up the server 14 | // 15 | // 16 | 17 | let app_state = app::get_app_state().await; 18 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 19 | let app = test::init_service( 20 | App::new() 21 | .app_data(web::Data::new(app_state.clone())) 22 | .service(web::scope("/api").configure(app::get_api_routes)), 23 | ) 24 | .await; 25 | 26 | // 27 | // 28 | // Step (1): Create post 29 | // 30 | // 31 | 32 | let body = post_controller::create::Body { 33 | title: mock_data::post::POST_TITLE.to_owned(), 34 | category_id: UNCLASSIFIED_CATEGORY_ID.to_owned(), 35 | content: mock_data::post::POST_CONTENT.to_owned(), 36 | }; 37 | 38 | let req = test::TestRequest::post() 39 | .uri("/api/post") 40 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 41 | .set_json(body.to_owned()) 42 | .to_request(); 43 | 44 | let resp = test::call_service(&app, req).await; 45 | 46 | assert!(resp.status().is_success()); 47 | 48 | let create_post_resp: post_controller::create::Response = test::read_body_json(resp).await; 49 | 50 | assert_eq!(create_post_resp.data.title, mock_data::post::POST_TITLE); 51 | assert_eq!(create_post_resp.data.content, mock_data::post::POST_CONTENT); 52 | assert_eq!(create_post_resp.data.category_id, UNCLASSIFIED_CATEGORY_ID); 53 | } 54 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/post/delete.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | 3 | use discuz_core::{ 4 | constants::{FAKE_ACCESS_TOKEN, UNCLASSIFIED_CATEGORY_ID}, 5 | migration::{Migrator, MigratorTrait}, 6 | service::{auth::constants::UserRole, user::user_service::UserServiceTrait}, 7 | utils::mock_data, 8 | }; 9 | use discuz_server::{app, post::post_controller, user::user_controller}; 10 | 11 | #[actix_web::test] 12 | async fn delete_post() { 13 | // 14 | // 15 | // Step (0): Set up the server 16 | // 17 | // 18 | 19 | let app_state = app::get_app_state().await; 20 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 21 | let app = test::init_service( 22 | App::new() 23 | .app_data(web::Data::new(app_state.clone())) 24 | .service(web::scope("/api").configure(app::get_api_routes)), 25 | ) 26 | .await; 27 | 28 | // 29 | // 30 | // Step (1): Create post 31 | // 32 | // 33 | 34 | let body = post_controller::create::Body { 35 | title: mock_data::post::POST_TITLE.to_owned(), 36 | category_id: UNCLASSIFIED_CATEGORY_ID.to_owned(), 37 | content: mock_data::post::POST_CONTENT.to_owned(), 38 | }; 39 | 40 | let req = test::TestRequest::post() 41 | .uri("/api/post") 42 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 43 | .set_json(body.to_owned()) 44 | .to_request(); 45 | 46 | let resp = test::call_service(&app, req).await; 47 | 48 | assert!(resp.status().is_success()); 49 | 50 | let create_post_resp: post_controller::create::Response = test::read_body_json(resp).await; 51 | let post_id = create_post_resp.data.id; 52 | 53 | // 54 | // 55 | // Step (2): Get User and update user role 56 | // 57 | // 58 | 59 | let req = test::TestRequest::get() 60 | .uri("/api/user/me") 61 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 62 | .to_request(); 63 | let resp = test::call_service(&app, req).await; 64 | assert!(resp.status().is_success()); 65 | 66 | let user_body: user_controller::get_me::Response = test::read_body_json(resp).await; 67 | let user_id = user_body.data.id; 68 | 69 | app_state 70 | .user_service 71 | .update_role(&user_id, &UserRole::Admin) 72 | .await 73 | .unwrap(); 74 | 75 | // 76 | // 77 | // Step (3): Get User and update user role 78 | // 79 | // 80 | 81 | let req = test::TestRequest::delete() 82 | .uri(&format!("/api/post/{post_id}")) 83 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 84 | .set_json(body.to_owned()) 85 | .to_request(); 86 | 87 | let resp = test::call_service(&app, req).await; 88 | 89 | assert!(resp.status().is_success()); 90 | 91 | // 92 | // 93 | // Step (4): Test if failure to retrieve the post after delete 94 | // 95 | // 96 | let req = test::TestRequest::get() 97 | .uri(&format!("/api/post/{post_id}")) 98 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 99 | .to_request(); 100 | 101 | let resp = test::call_service(&app, req).await; 102 | 103 | assert!(resp.status().is_client_error()); 104 | } 105 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/post/mod.rs: -------------------------------------------------------------------------------- 1 | mod create; 2 | mod delete; 3 | mod update; 4 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/user/get_me.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::{ 3 | constants::FAKE_ACCESS_TOKEN, 4 | migration::{Migrator, MigratorTrait}, 5 | service::auth::utils::mock_data, 6 | }; 7 | use discuz_server::{app, user::user_controller}; 8 | 9 | #[actix_web::test] 10 | async fn get_me() { 11 | let app_state = app::get_app_state().await; 12 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 13 | let app = test::init_service( 14 | App::new() 15 | .app_data(web::Data::new(app_state)) 16 | .service(web::scope("/api").configure(app::get_api_routes)), 17 | ) 18 | .await; 19 | let req = test::TestRequest::get() 20 | .uri("/api/user/me") 21 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 22 | .to_request(); 23 | let resp = test::call_service(&app, req).await; 24 | assert!(resp.status().is_success()); 25 | 26 | let body: user_controller::get_me::Response = test::read_body_json(resp).await; 27 | let mock_user = mock_data::get_mock_auth_user(); 28 | assert_eq!(body.data.name, mock_user.name); 29 | assert_eq!(body.data.email, mock_user.email); 30 | assert!(body.data.avatar_url.is_none()); 31 | } 32 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/user/get_me_401.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::migration::{Migrator, MigratorTrait}; 3 | use discuz_server::app; 4 | 5 | #[actix_web::test] 6 | async fn get_me_missing_authorization() { 7 | let app_state = app::get_app_state().await; 8 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 9 | let app = test::init_service( 10 | App::new() 11 | .app_data(web::Data::new(app_state)) 12 | .service(web::scope("/api").configure(app::get_api_routes)), 13 | ) 14 | .await; 15 | let req = test::TestRequest::get().uri("/api/user/me").to_request(); 16 | let resp = test::call_service(&app, req).await; 17 | assert!(resp.status().is_client_error()); 18 | assert_eq!(resp.status().as_u16(), 401); 19 | } 20 | 21 | #[actix_web::test] 22 | async fn get_me_invalid_access_token() { 23 | let app_state = app::get_app_state().await; 24 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 25 | let app = test::init_service( 26 | App::new() 27 | .app_data(web::Data::new(app_state)) 28 | .service(web::scope("/api").configure(app::get_api_routes)), 29 | ) 30 | .await; 31 | let req = test::TestRequest::get() 32 | .uri("/api/user/me") 33 | .append_header(("authorization", "basic FAKE_TOKEN".to_string())) 34 | .to_request(); 35 | let resp = test::call_service(&app, req).await; 36 | assert!(resp.status().is_client_error()); 37 | assert_eq!(resp.status().as_u16(), 401); 38 | } 39 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/user/get_me_403.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::{ 3 | constants::FAKE_ACCESS_TOKEN, 4 | migration::{Migrator, MigratorTrait}, 5 | service::prelude::*, 6 | }; 7 | use discuz_server::{app, user::user_controller}; 8 | 9 | #[actix_web::test] 10 | async fn get_me() { 11 | let app_state = app::get_app_state().await; 12 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 13 | let app = test::init_service( 14 | App::new() 15 | .app_data(web::Data::new(app_state.clone())) 16 | .service(web::scope("/api").configure(app::get_api_routes)), 17 | ) 18 | .await; 19 | 20 | let user_service = app_state.user_service.clone(); 21 | 22 | let req = test::TestRequest::get() 23 | .uri("/api/user/me") 24 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 25 | .to_request(); 26 | let resp = test::call_service(&app, req).await; 27 | assert!(resp.status().is_success()); 28 | 29 | let body: user_controller::get_me::Response = test::read_body_json(resp).await; 30 | let user_id = body.data.id; 31 | 32 | let input = BanUserInput { 33 | ban_user_id: user_id.to_owned(), 34 | ban_reason: None, 35 | ban_time: None, 36 | user_id: user_id.to_owned(), 37 | }; 38 | 39 | user_service.ban_user_account(&input).await.unwrap(); 40 | 41 | let req = test::TestRequest::get() 42 | .uri("/api/user/me") 43 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 44 | .to_request(); 45 | let resp = test::call_service(&app, req).await; 46 | assert_eq!(resp.status().as_u16(), 403); 47 | } 48 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/user/mod.rs: -------------------------------------------------------------------------------- 1 | mod get_me; 2 | mod get_me_401; 3 | mod get_me_403; 4 | mod patch_user; 5 | -------------------------------------------------------------------------------- /apps/server/tests/integrations/user/patch_user.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{test, web, App}; 2 | use discuz_core::{ 3 | constants::FAKE_ACCESS_TOKEN, 4 | migration::{Migrator, MigratorTrait}, 5 | }; 6 | use discuz_server::{app, user::user_controller}; 7 | 8 | #[actix_web::test] 9 | async fn patch_user() { 10 | let app_state = app::get_app_state().await; 11 | Migrator::refresh(&app_state.db_connection).await.unwrap(); 12 | let app = test::init_service( 13 | App::new() 14 | .app_data(web::Data::new(app_state)) 15 | .service(web::scope("/api").configure(app::get_api_routes)), 16 | ) 17 | .await; 18 | 19 | // Step 1: Get the user 20 | let req = test::TestRequest::get() 21 | .uri("/api/user/me") 22 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 23 | .to_request(); 24 | let resp = test::call_service(&app, req).await; 25 | assert!(resp.status().is_success()); 26 | 27 | let get_me_resp: user_controller::get_me::Response = test::read_body_json(resp).await; 28 | assert!(get_me_resp.data.avatar_url.is_none()); 29 | 30 | let new_user_name = "New User Name"; 31 | let new_avatar_url = "http://avatar.com/icon.png"; 32 | 33 | let body = user_controller::update_user::Body { 34 | name: new_user_name.to_owned(), 35 | avatar_url: Some(new_avatar_url.to_owned()), 36 | }; 37 | 38 | // Step 2: Update user information 39 | let req = test::TestRequest::patch() 40 | .uri(&format!("/api/user/{}", get_me_resp.data.id)) 41 | .append_header(("authorization", "bearer ".to_string() + FAKE_ACCESS_TOKEN)) 42 | .set_json(body) 43 | .to_request(); 44 | let resp = test::call_service(&app, req).await; 45 | assert!(resp.status().is_success()); 46 | 47 | let patch_user_resp: user_controller::update_user::Response = test::read_body_json(resp).await; 48 | assert!(patch_user_resp.data.avatar_url.is_some()); 49 | assert_eq!(patch_user_resp.data.avatar_url.unwrap(), new_avatar_url); 50 | assert_eq!(patch_user_resp.data.name, new_user_name); 51 | } 52 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.65.0" 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: master 3 | ignore: 4 | # Db migration allowed for production only 5 | - "apps/db-migration" 6 | # Disable on project entry point 7 | - "**/main.rs" 8 | 9 | # 10 | # 11 | # Program specific 12 | # 13 | # 14 | 15 | # 16 | # Those scripts are never used in non-production environments, so the code coverage never runs it. 17 | # 18 | - "apps/server/src/auth/auth_controller/get_tokens.rs" 19 | - "libs/utils/src/redis.rs" 20 | - "libs/utils/src/config.rs" 21 | # Relation is never used in test 22 | - "libs/core/src/repository/database/entities" 23 | - "libs/core/src/utils/db_tools.rs" 24 | - "libs/core/src/service/file/provider" 25 | - "libs/actix-helmet" 26 | -------------------------------------------------------------------------------- /config/ci.toml: -------------------------------------------------------------------------------- 1 | run_mode = "ci" 2 | 3 | [database] 4 | hostname = "localhost" 5 | username = "mysql" 6 | password = "mysql" 7 | port = 3306 8 | database = "discuz" 9 | -------------------------------------------------------------------------------- /config/default.toml: -------------------------------------------------------------------------------- 1 | # Default settings 2 | run_mode = "testing" 3 | 4 | [database] 5 | hostname = "" 6 | username = "" 7 | password = "" 8 | port = 3306 9 | database = "" 10 | 11 | [app] 12 | port = 3100 13 | 14 | [amazon] 15 | region = "ca-central-1" 16 | 17 | [amazon.cognito] 18 | user_pool_id = "" 19 | domain = "" 20 | client_id = "" 21 | redirect_uri = "http://localhost:3000" 22 | 23 | [amazon.s3] 24 | bucket = "" 25 | 26 | [redis] 27 | enable = false 28 | -------------------------------------------------------------------------------- /config/testing.toml: -------------------------------------------------------------------------------- 1 | # Default settings 2 | run_mode = "testing" 3 | 4 | [database] 5 | url = "sqlite::memory:" # Using Sqlite for unit testing 6 | 7 | [amazon] 8 | region = "ca-central-1" 9 | 10 | [amazon.cognito] 11 | user_pool_id = "" 12 | domain = "" 13 | client_id = "" 14 | redirect_uri = "http://localhost:3000" 15 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "ignorePaths": [ 4 | "**/main.rs", 5 | "./target", 6 | "./.circleci/config.yml", 7 | "./libs/core/src/utils/mock_data" 8 | ], 9 | "dictionaryDefinitions": [], 10 | "dictionaries": [], 11 | "words": [ 12 | "actix", 13 | "alkonost", 14 | "artix", 15 | "chrono", 16 | "clippy", 17 | "cobertura", 18 | "codecov", 19 | "codegen", 20 | "cognitoidentityprovider", 21 | "discuz", 22 | "dotenv", 23 | "grcov", 24 | "iden", 25 | "inflector", 26 | "jsonwebtoken", 27 | "kebabcase", 28 | "kenyipp", 29 | "lcov", 30 | "msrv", 31 | "nextest", 32 | "nohup", 33 | "noopen", 34 | "opensource", 35 | "postable", 36 | "prerequest", 37 | "presigning", 38 | "remaning", 39 | "reqwest", 40 | "RUSTFLAGS", 41 | "rustup", 42 | "SAMEORIGIN", 43 | "serde", 44 | "sqlx", 45 | "thiserror", 46 | "ulimits", 47 | "whatsoo" 48 | ], 49 | "ignoreWords": [], 50 | "import": [] 51 | } 52 | -------------------------------------------------------------------------------- /docker-compose.sample.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | 5 | server: 6 | container_name: discuz_server 7 | build: ./ 8 | restart: unless-stopped 9 | ports: 10 | - 3100:7800 11 | command: cargo run --release --bin discuz-server 12 | logging: 13 | driver: "json-file" 14 | options: 15 | max-size: "200k" # 200kb 16 | max-file: "2" 17 | ulimits: 18 | core: 1024 19 | 20 | mysql: 21 | container_name: discuz_mysql 22 | image: mysql:5.7 23 | ports: 24 | - 3306:3306 25 | volumes: 26 | - ./data:/var/lib/mysql 27 | environment: 28 | - MYSQL_ROOT_PASSWORD=discuz 29 | - MYSQL_USER=discuz 30 | - MYSQL_PASSWORD=discuz 31 | - MYSQL_DATABASE=discuz 32 | 33 | networks: 34 | default: 35 | external: 36 | name: discuz 37 | -------------------------------------------------------------------------------- /libs/.gitignore: -------------------------------------------------------------------------------- 1 | # The main.rs is used for debugging only for the libraries' plugins 2 | **/src/main.rs 3 | -------------------------------------------------------------------------------- /libs/actix-helmet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-helmet" 3 | description = "The `actix-helmet` is a Rust library that offers security enhancements for your Actix-web application by setting important HTTP headers." 4 | license = "MIT" 5 | version = "0.1.0" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | Inflector = "0.11.4" 10 | actix-web = "4.3.0" 11 | futures-util = "0.3.26" 12 | -------------------------------------------------------------------------------- /libs/actix-helmet/Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RUN_MODE = "testing" 3 | CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true 4 | 5 | [tasks.dev] 6 | workspace = false 7 | env = { RUN_MODE = "development", RUST_BACKTRACE = "0" } 8 | command = "cargo" 9 | args = ["watch", "-x run"] 10 | 11 | [tasks.test] 12 | env = { RUN_MODE = "testing", RUST_BACKTRACE = "0", RUSTFLAGS = "-A unused_imports" } 13 | command = "cargo" 14 | args = ["nextest", "run", "--no-capture"] 15 | -------------------------------------------------------------------------------- /libs/actix-helmet/src/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{inner::Inner, middleware::HelmetMiddleware, policies::StrictTransportSecurity}; 2 | use actix_web::{ 3 | dev::{Service, ServiceRequest, ServiceResponse, Transform}, 4 | Error, 5 | }; 6 | use std::{ 7 | future::{ready, Ready}, 8 | rc::Rc, 9 | }; 10 | 11 | #[non_exhaustive] 12 | #[derive(Clone, Default)] 13 | pub struct Helmet { 14 | inner: Rc, 15 | } 16 | 17 | impl Helmet { 18 | pub fn enable_strict_transport_security( 19 | mut self, 20 | max_age: u32, 21 | include_sub_domains: bool, 22 | preload: bool, 23 | ) -> Helmet { 24 | if let Some(helmet) = Rc::get_mut(&mut self.inner) { 25 | let strict_transport_security = StrictTransportSecurity { 26 | max_age, 27 | include_sub_domains, 28 | preload, 29 | }; 30 | helmet.strict_transport_security = Some(strict_transport_security); 31 | } 32 | self 33 | } 34 | 35 | pub fn disable_strict_transport_security(mut self) -> Helmet { 36 | if let Some(helmet) = Rc::get_mut(&mut self.inner) { 37 | helmet.strict_transport_security = None; 38 | } 39 | self 40 | } 41 | } 42 | 43 | impl Transform for Helmet 44 | where 45 | S: Service, Error = Error>, 46 | S::Future: 'static, 47 | B: 'static, 48 | { 49 | type Response = ServiceResponse; 50 | type Error = Error; 51 | type InitError = (); 52 | type Transform = HelmetMiddleware; 53 | type Future = Ready>; 54 | 55 | fn new_transform(&self, service: S) -> Self::Future { 56 | let middleware = HelmetMiddleware { 57 | service, 58 | inner: self.inner.clone(), 59 | }; 60 | ready(Ok(middleware)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /libs/actix-helmet/src/inner.rs: -------------------------------------------------------------------------------- 1 | use crate::policies::{ 2 | ReferrerPolicy, StrictTransportSecurity, XContentTypeOptions, XDnsPrefetchControl, 3 | XDownloadOptions, XFrameOptions, XPermittedCrossDomainPolicies, XXssProtection, 4 | }; 5 | use std::default::Default; 6 | 7 | pub struct Inner { 8 | pub x_xss_protection: XXssProtection, 9 | pub referrer_policy: ReferrerPolicy, 10 | pub x_frame_options: XFrameOptions, 11 | pub x_permitted_cross_domain_policies: XPermittedCrossDomainPolicies, 12 | pub x_dn_prefetch_control: XDnsPrefetchControl, 13 | pub x_download_options: XDownloadOptions, 14 | pub x_content_type_options: XContentTypeOptions, 15 | pub strict_transport_security: Option, 16 | } 17 | 18 | impl Default for Inner { 19 | fn default() -> Self { 20 | Inner { 21 | x_xss_protection: XXssProtection::default(), 22 | referrer_policy: ReferrerPolicy::default(), 23 | x_frame_options: XFrameOptions::default(), 24 | x_permitted_cross_domain_policies: XPermittedCrossDomainPolicies::default(), 25 | x_dn_prefetch_control: XDnsPrefetchControl::default(), 26 | x_download_options: XDownloadOptions::Enable, 27 | x_content_type_options: XContentTypeOptions::Enable, 28 | strict_transport_security: Some(StrictTransportSecurity::default()), 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /libs/actix-helmet/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod builder; 2 | pub mod inner; 3 | pub mod middleware; 4 | pub mod policies; 5 | pub use builder::Helmet; 6 | -------------------------------------------------------------------------------- /libs/actix-helmet/src/middleware.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | inner::Inner, 3 | policies::{PolicyTrait, XContentTypeOptions, XDownloadOptions}, 4 | }; 5 | use actix_web::{ 6 | dev::{forward_ready, Service, ServiceRequest, ServiceResponse}, 7 | http::header::{self, HeaderName, HeaderValue}, 8 | Error, 9 | }; 10 | use futures_util::future::LocalBoxFuture; 11 | use std::rc::Rc; 12 | 13 | pub struct HelmetMiddleware { 14 | pub service: S, 15 | pub inner: Rc, 16 | } 17 | 18 | impl HelmetMiddleware { 19 | fn augment_response(mut res: ServiceResponse, inner: &Inner) -> ServiceResponse { 20 | let header_map = res.headers_mut(); 21 | header_map.insert( 22 | header::X_XSS_PROTECTION, 23 | inner.x_xss_protection.get_header_value(), 24 | ); 25 | header_map.insert( 26 | header::REFERRER_POLICY, 27 | inner.referrer_policy.get_header_value(), 28 | ); 29 | header_map.insert( 30 | header::X_FRAME_OPTIONS, 31 | inner.x_frame_options.get_header_value(), 32 | ); 33 | if inner.x_download_options == XDownloadOptions::Enable { 34 | header_map.insert( 35 | HeaderName::from_lowercase(b"x-download-options").unwrap(), 36 | HeaderValue::from_static("noopen"), 37 | ); 38 | } 39 | header_map.insert( 40 | HeaderName::from_lowercase(b"x-permitted-cross-domain-policies").unwrap(), 41 | inner.x_permitted_cross_domain_policies.get_header_value(), 42 | ); 43 | header_map.insert( 44 | header::X_DNS_PREFETCH_CONTROL, 45 | inner.x_dn_prefetch_control.get_header_value(), 46 | ); 47 | if inner.x_content_type_options == XContentTypeOptions::Enable { 48 | header_map.insert( 49 | header::X_CONTENT_TYPE_OPTIONS, 50 | HeaderValue::from_static("nosniff"), 51 | ); 52 | } 53 | if let Some(strict_transport_security) = inner.strict_transport_security.to_owned() { 54 | header_map.insert( 55 | header::STRICT_TRANSPORT_SECURITY, 56 | strict_transport_security.get_header_value(), 57 | ); 58 | } 59 | res 60 | } 61 | } 62 | 63 | impl Service for HelmetMiddleware 64 | where 65 | S: Service, Error = Error>, 66 | S::Future: 'static, 67 | B: 'static, 68 | { 69 | type Response = ServiceResponse; 70 | type Error = Error; 71 | type Future = LocalBoxFuture<'static, Result>; 72 | 73 | fn call(&self, req: ServiceRequest) -> Self::Future { 74 | let fut = self.service.call(req); 75 | let inner = Rc::clone(&self.inner); 76 | Box::pin(async move { 77 | let res = fut.await?; 78 | Ok(Self::augment_response(res, &inner)) 79 | }) 80 | } 81 | 82 | forward_ready!(service); 83 | } 84 | -------------------------------------------------------------------------------- /libs/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "discuz-core" 3 | version = "0.1.0" 4 | authors = ["Ken "] 5 | rust-version = "1.65.0" 6 | description = "The main business logic of this application" 7 | edition = "2021" 8 | license = "ISC" 9 | keywords = ["discuz", "core"] 10 | 11 | [dependencies] 12 | async-recursion = "1.0.2" 13 | async-trait = "0.1.60" 14 | aws-config = "0.52.0" 15 | aws-sdk-cognitoidentityprovider = "0.22.0" 16 | aws-sdk-s3 = "0.22.0" 17 | chrono = "0.4.23" 18 | derive_more = "0.99.17" 19 | discuz-utils = { path = "../utils" } 20 | dotenv = "0.15.0" 21 | error-stack = "0.2.4" 22 | fake = "2.5.0" 23 | fred = "5.2.0" 24 | jsonwebtoken = "8.2.0" 25 | lazy_static = "1.4.0" 26 | reqwest = { version = "0.11.13", features = ["json"] } 27 | sea-orm = { version = "0.10.5", features = [ 28 | "sqlx-mysql", 29 | "sqlx-sqlite", 30 | "runtime-tokio-native-tls", 31 | ] } 32 | sea-orm-migration = "0.10.5" 33 | serde = { version = "1.0.150", features = ["derive"] } 34 | serde_json = "1.0.89" 35 | slugify = "0.1.0" 36 | strum = "0.24.1" 37 | strum_macros = "0.24.3" 38 | tokio = { version = "1", features = ["full"] } 39 | tracing = "0.1" 40 | uuid = "1.2.2" 41 | 42 | [dev-dependencies] 43 | -------------------------------------------------------------------------------- /libs/core/Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | RUN_MODE = "testing" 3 | CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true 4 | 5 | [tasks.dev] 6 | workspace = false 7 | env = { RUN_MODE = "development", RUST_BACKTRACE = "0" } 8 | command = "cargo" 9 | args = ["watch", "-x run"] 10 | 11 | [tasks.test] 12 | env = { RUN_MODE = "testing", RUST_BACKTRACE = "0", RUSTFLAGS = "-A unused_imports" } 13 | command = "cargo" 14 | args = ["nextest", "run", "--no-capture"] 15 | -------------------------------------------------------------------------------- /libs/core/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const BYTES_OF_1_MB: i64 = 1000000; 2 | pub const UNCLASSIFIED_CATEGORY_ID: &str = "01c73ef8-8589-4f7b-9c32-cf01d3a0121a"; 3 | pub const FAKE_SUB: &str = "FAKE_SUB"; 4 | pub const FAKE_ACCESS_TOKEN: &str = "FAKE_ACCESS_TOKEN"; 5 | pub const MAX_POST_REPLY_COUNT: i32 = 1000; 6 | -------------------------------------------------------------------------------- /libs/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate async_trait; 3 | 4 | #[macro_use] 5 | extern crate lazy_static; 6 | 7 | #[macro_use] 8 | extern crate async_recursion; 9 | 10 | #[macro_use] 11 | extern crate serde; 12 | 13 | pub mod migration; 14 | pub mod repository; 15 | pub mod service; 16 | 17 | pub mod constants; 18 | pub mod utils; 19 | -------------------------------------------------------------------------------- /libs/core/src/migration/m_20221217_000001_create_user_table.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::db_tools::on_update_current_timestamp; 2 | use sea_orm_migration::prelude::*; 3 | 4 | pub struct Migration; 5 | 6 | impl MigrationName for Migration { 7 | fn name(&self) -> &str { 8 | "m_20221217-000001_create_user_table" 9 | } 10 | } 11 | 12 | #[async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | // Create the user table 16 | manager 17 | .create_table( 18 | Table::create() 19 | .table(User::Table) 20 | .col( 21 | ColumnDef::new(User::UserId) 22 | .string() 23 | .string_len(64) 24 | .not_null() 25 | .primary_key(), 26 | ) 27 | .col(ColumnDef::new(User::Sub).string().string_len(64).not_null()) 28 | .col( 29 | ColumnDef::new(User::Name) 30 | .string() 31 | .string_len(150) 32 | .not_null(), 33 | ) 34 | .col( 35 | ColumnDef::new(User::Email) 36 | .string() 37 | .string_len(250) 38 | .not_null() 39 | .unique_key(), 40 | ) 41 | .col(ColumnDef::new(User::Phone).string().string_len(150)) 42 | .col( 43 | ColumnDef::new(User::NoOfBan) 44 | .integer() 45 | .unsigned() 46 | .not_null() 47 | .default(0), 48 | ) 49 | .col(ColumnDef::new(User::AvatarUrl).text()) 50 | .col(ColumnDef::new(User::Notes).text()) 51 | .col( 52 | ColumnDef::new(User::StatusId) 53 | .string() 54 | .string_len(15) 55 | .not_null() 56 | .default("normal"), 57 | ) 58 | .col( 59 | ColumnDef::new(User::CreatedAt) 60 | .timestamp() 61 | .extra("DEFAULT CURRENT_TIMESTAMP".to_owned()), 62 | ) 63 | .col( 64 | ColumnDef::new(User::UpdatedAt) 65 | .timestamp() 66 | .extra(on_update_current_timestamp(manager)), 67 | ) 68 | .to_owned(), 69 | ) 70 | .await?; 71 | Ok(()) 72 | } 73 | 74 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 75 | manager 76 | .drop_table(Table::drop().table(User::Table).to_owned()) 77 | .await?; 78 | Ok(()) 79 | } 80 | } 81 | 82 | #[derive(Iden)] 83 | pub enum User { 84 | Table, 85 | Sub, 86 | UserId, 87 | Name, 88 | Email, 89 | Phone, 90 | NoOfBan, 91 | Notes, 92 | AvatarUrl, 93 | StatusId, 94 | CreatedAt, 95 | UpdatedAt, 96 | } 97 | -------------------------------------------------------------------------------- /libs/core/src/migration/m_20230111_000002_create_file_table.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::db_tools::on_update_current_timestamp; 2 | use sea_orm_migration::prelude::*; 3 | 4 | pub struct Migration; 5 | 6 | impl MigrationName for Migration { 7 | fn name(&self) -> &str { 8 | "m_20230111_000002_create_file_table" 9 | } 10 | } 11 | 12 | #[async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .create_table( 17 | Table::create() 18 | .table(File::Table) 19 | .col( 20 | ColumnDef::new(File::FileId) 21 | .string() 22 | .string_len(64) 23 | .not_null() 24 | .primary_key(), 25 | ) 26 | .col( 27 | ColumnDef::new(File::Name) 28 | .string() 29 | .string_len(150) 30 | .not_null(), 31 | ) 32 | .col(ColumnDef::new(File::AlternativeText).text().null()) 33 | .col(ColumnDef::new(File::Caption).text().null()) 34 | .col(ColumnDef::new(File::Description).text().null()) 35 | .col(ColumnDef::new(File::UserId).string().string_len(64).null()) 36 | .col( 37 | ColumnDef::new(File::MimeType) 38 | .string() 39 | .string_len(64) 40 | .null(), 41 | ) 42 | .col( 43 | ColumnDef::new(File::Size) 44 | .decimal() 45 | .decimal_len(8, 2) 46 | .null(), 47 | ) 48 | .col(ColumnDef::new(File::PublicUri).text().null()) 49 | .col( 50 | ColumnDef::new(File::StatusId) 51 | .string() 52 | .string_len(1) 53 | .not_null() 54 | .default("A"), 55 | ) 56 | .col( 57 | ColumnDef::new(File::CreatedAt) 58 | .timestamp() 59 | .extra("DEFAULT CURRENT_TIMESTAMP".to_owned()), 60 | ) 61 | .col( 62 | ColumnDef::new(File::UpdatedAt) 63 | .timestamp() 64 | .extra(on_update_current_timestamp(manager)), 65 | ) 66 | .foreign_key( 67 | ForeignKey::create() 68 | .name("FK-file-user_id-user-user_id") 69 | .from(File::Table, File::UserId) 70 | .to(User::Table, User::UserId), 71 | ) 72 | .to_owned(), 73 | ) 74 | .await?; 75 | Ok(()) 76 | } 77 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 78 | manager 79 | .drop_table(Table::drop().table(File::Table).to_owned()) 80 | .await?; 81 | Ok(()) 82 | } 83 | } 84 | 85 | #[derive(Iden)] 86 | enum User { 87 | Table, 88 | UserId, 89 | } 90 | 91 | #[derive(Iden)] 92 | pub enum File { 93 | Table, 94 | FileId, 95 | Name, 96 | AlternativeText, 97 | Caption, 98 | Description, 99 | MimeType, 100 | Size, 101 | PublicUri, 102 | UserId, 103 | StatusId, 104 | CreatedAt, 105 | UpdatedAt, 106 | } 107 | -------------------------------------------------------------------------------- /libs/core/src/migration/m_20230113_000003_create_category_table.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::db_tools::on_update_current_timestamp; 2 | use sea_orm::DbBackend; 3 | use sea_orm_migration::prelude::*; 4 | 5 | pub struct Migration; 6 | 7 | impl MigrationName for Migration { 8 | fn name(&self) -> &str { 9 | "m_20230113_000003_create_category_table" 10 | } 11 | } 12 | 13 | #[async_trait] 14 | impl MigrationTrait for Migration { 15 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 16 | manager.create_table(create_category(manager)).await?; 17 | 18 | if manager.get_database_backend() == DbBackend::MySql { 19 | manager 20 | .create_foreign_key( 21 | ForeignKey::create() 22 | .name("FK-category-parent_id-category-category_id") 23 | .from(Category::Table, Category::ParentId) 24 | .to(Category::Table, Category::CategoryId) 25 | .to_owned(), 26 | ) 27 | .await?; 28 | } 29 | Ok(()) 30 | } 31 | 32 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 33 | manager 34 | .drop_table(Table::drop().table(Category::Table).to_owned()) 35 | .await?; 36 | Ok(()) 37 | } 38 | } 39 | 40 | fn create_category(manager: &SchemaManager) -> TableCreateStatement { 41 | Table::create() 42 | .table(Category::Table) 43 | .col( 44 | ColumnDef::new(Category::CategoryId) 45 | .string() 46 | .string_len(64) 47 | .not_null() 48 | .primary_key(), 49 | ) 50 | .col( 51 | ColumnDef::new(Category::Name) 52 | .string() 53 | .string_len(50) 54 | .not_null(), 55 | ) 56 | .col( 57 | ColumnDef::new(Category::Slug) 58 | .string() 59 | .string_len(150) 60 | .unique_key() 61 | .not_null(), 62 | ) 63 | .col(ColumnDef::new(Category::Description).text()) 64 | .col( 65 | ColumnDef::new(Category::Level) 66 | .integer() 67 | .integer_len(3) 68 | .unsigned() 69 | .default(1), 70 | ) 71 | .col(ColumnDef::new(Category::Postable).boolean().default(true)) 72 | .col( 73 | ColumnDef::new(Category::ParentId) 74 | .string() 75 | .string_len(64) 76 | .null(), 77 | ) 78 | .col(ColumnDef::new(Category::Count).integer().default(0)) 79 | .col(ColumnDef::new(Category::SortIndex).integer().default(100)) 80 | .col( 81 | ColumnDef::new(Category::UserId) 82 | .string() 83 | .string_len(64) 84 | .null(), 85 | ) 86 | .col( 87 | ColumnDef::new(Category::StatusId) 88 | .string() 89 | .string_len(1) 90 | .not_null() 91 | .default("A"), 92 | ) 93 | .col( 94 | ColumnDef::new(Category::CreatedAt) 95 | .timestamp() 96 | .extra("DEFAULT CURRENT_TIMESTAMP".to_owned()), 97 | ) 98 | .col( 99 | ColumnDef::new(Category::UpdatedAt) 100 | .timestamp() 101 | .extra(on_update_current_timestamp(manager)), 102 | ) 103 | .foreign_key( 104 | ForeignKey::create() 105 | .name("FK-category-user_id-user-user_id") 106 | .from(Category::Table, Category::UserId) 107 | .to(User::Table, User::UserId), 108 | ) 109 | .to_owned() 110 | } 111 | 112 | #[derive(Iden)] 113 | pub enum Category { 114 | Table, 115 | CategoryId, 116 | Name, 117 | Slug, 118 | Description, 119 | Level, 120 | Postable, 121 | ParentId, 122 | Count, 123 | SortIndex, 124 | UserId, 125 | StatusId, 126 | CreatedAt, 127 | UpdatedAt, 128 | } 129 | 130 | #[derive(Iden)] 131 | enum User { 132 | Table, 133 | UserId, 134 | } 135 | -------------------------------------------------------------------------------- /libs/core/src/migration/m_20230113_000004_create_post_table.rs: -------------------------------------------------------------------------------- 1 | use crate::{constants::MAX_POST_REPLY_COUNT, utils::db_tools::on_update_current_timestamp}; 2 | use sea_orm_migration::prelude::*; 3 | 4 | pub struct Migration; 5 | 6 | impl MigrationName for Migration { 7 | fn name(&self) -> &str { 8 | "m_20230113_000004_create_post_table" 9 | } 10 | } 11 | 12 | #[async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager.create_table(create_post(manager)).await?; 16 | Ok(()) 17 | } 18 | 19 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 20 | manager 21 | .drop_table(Table::drop().table(Post::Table).to_owned()) 22 | .await?; 23 | Ok(()) 24 | } 25 | } 26 | 27 | fn create_post(manager: &SchemaManager) -> TableCreateStatement { 28 | Table::create() 29 | .table(Post::Table) 30 | .col( 31 | ColumnDef::new(Post::PostId) 32 | .integer() 33 | .integer_len(11) 34 | .unsigned() 35 | .not_null() 36 | .primary_key() 37 | .auto_increment(), 38 | ) 39 | .col( 40 | ColumnDef::new(Post::Title) 41 | .string() 42 | .string_len(150) 43 | .not_null(), 44 | ) 45 | .col( 46 | ColumnDef::new(Post::CommentCount) 47 | .integer() 48 | .integer_len(7) 49 | .unsigned() 50 | .default(0) 51 | .not_null(), 52 | ) 53 | .col( 54 | ColumnDef::new(Post::MaxCommentCount) 55 | .integer() 56 | .integer_len(7) 57 | .unsigned() 58 | .default(MAX_POST_REPLY_COUNT) 59 | .not_null(), 60 | ) 61 | .col( 62 | ColumnDef::new(Post::Slug) 63 | .string() 64 | .string_len(150) 65 | .unique_key() 66 | .not_null(), 67 | ) 68 | .col( 69 | ColumnDef::new(Post::CategoryId) 70 | .string() 71 | .string_len(64) 72 | .not_null(), 73 | ) 74 | .col(ColumnDef::new(Post::Content).text()) 75 | .col( 76 | ColumnDef::new(Post::UserId) 77 | .string() 78 | .string_len(64) 79 | .not_null(), 80 | ) 81 | .col( 82 | ColumnDef::new(Post::StatusId) 83 | .string() 84 | .string_len(1) 85 | .not_null() 86 | .default("A"), 87 | ) 88 | .col( 89 | ColumnDef::new(Post::CreatedAt) 90 | .timestamp() 91 | .extra("DEFAULT CURRENT_TIMESTAMP".to_owned()), 92 | ) 93 | .col( 94 | ColumnDef::new(Post::UpdatedAt) 95 | .timestamp() 96 | .extra(on_update_current_timestamp(manager)), 97 | ) 98 | .foreign_key( 99 | ForeignKey::create() 100 | .name("FK-post-user_id-user-user_id") 101 | .from(Post::Table, Post::UserId) 102 | .to(User::Table, User::UserId), 103 | ) 104 | .foreign_key( 105 | ForeignKey::create() 106 | .name("FK-post-category_id-category-category_id") 107 | .from(Post::Table, Post::UserId) 108 | .to(User::Table, User::UserId), 109 | ) 110 | .to_owned() 111 | } 112 | 113 | #[derive(Iden)] 114 | pub enum Post { 115 | Table, 116 | PostId, 117 | Title, 118 | Slug, 119 | CategoryId, 120 | Content, 121 | CommentCount, 122 | MaxCommentCount, 123 | UserId, 124 | StatusId, 125 | CreatedAt, 126 | UpdatedAt, 127 | } 128 | 129 | #[derive(Iden)] 130 | enum User { 131 | Table, 132 | UserId, 133 | } 134 | -------------------------------------------------------------------------------- /libs/core/src/migration/m_20230114_000005_create_user_role_column.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::db_tools; 2 | use sea_orm_migration::prelude::*; 3 | 4 | pub struct Migration; 5 | 6 | impl MigrationName for Migration { 7 | fn name(&self) -> &str { 8 | "m_20230114_000005_create_user_role_column" 9 | } 10 | } 11 | 12 | #[async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager 16 | .alter_table( 17 | sea_query::Table::alter() 18 | .table(Alias::new("user")) 19 | .add_column( 20 | ColumnDef::new(Alias::new("role")) 21 | .string() 22 | .default("user") 23 | .not_null() 24 | .extra(db_tools::add_column_after(manager, "sub")), 25 | ) 26 | .to_owned(), 27 | ) 28 | .await?; 29 | Ok(()) 30 | } 31 | 32 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 33 | manager 34 | .alter_table( 35 | sea_query::Table::alter() 36 | .table(Alias::new("user")) 37 | .drop_column(Alias::new("role")) 38 | .to_owned(), 39 | ) 40 | .await?; 41 | Ok(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /libs/core/src/migration/m_20230124_000007_create_user_ban_history_table.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::db_tools::on_update_current_timestamp; 2 | use sea_orm::DbBackend; 3 | use sea_orm_migration::prelude::*; 4 | 5 | pub struct Migration; 6 | 7 | impl MigrationName for Migration { 8 | fn name(&self) -> &str { 9 | "m_20230124_000007_create_user_ban_history_table" 10 | } 11 | } 12 | 13 | #[async_trait] 14 | impl MigrationTrait for Migration { 15 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 16 | manager 17 | .create_table(create_user_ban_history_table(manager)) 18 | .await?; 19 | 20 | if manager.get_database_backend() == DbBackend::MySql { 21 | manager 22 | .create_index( 23 | Index::create() 24 | .table(UserBanHistory::Table) 25 | .name("IDX-user_ban_history-release_time") 26 | .index_type(IndexType::BTree) 27 | .col(UserBanHistory::ReleaseTime) 28 | .to_owned(), 29 | ) 30 | .await?; 31 | manager 32 | .create_foreign_key( 33 | ForeignKey::create() 34 | .name("FK-user_ban_history-ban_user_id-user-user_id") 35 | .from(UserBanHistory::Table, UserBanHistory::BanUserId) 36 | .to(User::Table, User::UserId) 37 | .to_owned(), 38 | ) 39 | .await?; 40 | manager 41 | .create_foreign_key( 42 | ForeignKey::create() 43 | .name("FK-user_ban_history-user_id-user-user_id") 44 | .from(UserBanHistory::Table, UserBanHistory::UserId) 45 | .to(User::Table, User::UserId) 46 | .to_owned(), 47 | ) 48 | .await?; 49 | } 50 | 51 | Ok(()) 52 | } 53 | 54 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 55 | manager 56 | .drop_table(Table::drop().table(UserBanHistory::Table).to_owned()) 57 | .await?; 58 | Ok(()) 59 | } 60 | } 61 | 62 | fn create_user_ban_history_table(manager: &SchemaManager) -> TableCreateStatement { 63 | Table::create() 64 | .table(UserBanHistory::Table) 65 | .col( 66 | ColumnDef::new(UserBanHistory::UserBanHistoryId) 67 | .integer() 68 | .integer_len(11) 69 | .unsigned() 70 | .not_null() 71 | .auto_increment() 72 | .primary_key(), 73 | ) 74 | .col( 75 | ColumnDef::new(UserBanHistory::BanUserId) 76 | .string() 77 | .string_len(64) 78 | .not_null(), 79 | ) 80 | .col( 81 | ColumnDef::new(UserBanHistory::BanReason) 82 | .string() 83 | .string_len(128) 84 | .null(), 85 | ) 86 | .col( 87 | ColumnDef::new(UserBanHistory::BanTime) 88 | .integer() 89 | .unsigned() 90 | .null(), 91 | ) 92 | .col( 93 | ColumnDef::new(UserBanHistory::ReleaseTime) 94 | .timestamp() 95 | .null(), 96 | ) 97 | .col( 98 | ColumnDef::new(UserBanHistory::UserId) 99 | .string() 100 | .string_len(64) 101 | .null(), 102 | ) 103 | .col( 104 | ColumnDef::new(UserBanHistory::StatusId) 105 | .string() 106 | .string_len(1) 107 | .not_null() 108 | .default("A"), 109 | ) 110 | .col( 111 | ColumnDef::new(UserBanHistory::CreatedAt) 112 | .timestamp() 113 | .extra("DEFAULT CURRENT_TIMESTAMP".to_owned()), 114 | ) 115 | .col( 116 | ColumnDef::new(UserBanHistory::UpdatedAt) 117 | .timestamp() 118 | .extra(on_update_current_timestamp(manager)), 119 | ) 120 | .to_owned() 121 | } 122 | 123 | #[derive(Iden)] 124 | pub enum UserBanHistory { 125 | Table, 126 | UserBanHistoryId, 127 | BanUserId, 128 | BanReason, 129 | BanTime, 130 | ReleaseTime, 131 | UserId, 132 | StatusId, 133 | CreatedAt, 134 | UpdatedAt, 135 | } 136 | 137 | #[derive(Iden)] 138 | enum User { 139 | Table, 140 | UserId, 141 | } 142 | -------------------------------------------------------------------------------- /libs/core/src/migration/m_20230131_000009_create_apps_version_table.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::db_tools::{create_unique_key, on_update_current_timestamp}; 2 | use sea_orm::ConnectionTrait; 3 | use sea_orm_migration::prelude::*; 4 | 5 | pub struct Migration; 6 | 7 | impl MigrationName for Migration { 8 | fn name(&self) -> &str { 9 | "m_20230131_000009_create_apps_version" 10 | } 11 | } 12 | 13 | #[async_trait] 14 | impl MigrationTrait for Migration { 15 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 16 | manager 17 | .create_table(create_apps_version_table(manager)) 18 | .await?; 19 | 20 | manager 21 | .get_connection() 22 | .execute(create_unique_key( 23 | &AppsVersion::Table.to_string(), 24 | "UK_platform_package_id", 25 | vec![ 26 | AppsVersion::Platform.to_string(), 27 | AppsVersion::PackageId.to_string(), 28 | ], 29 | manager.get_database_backend(), 30 | )) 31 | .await 32 | .unwrap(); 33 | 34 | Ok(()) 35 | } 36 | 37 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 38 | manager 39 | .drop_table(Table::drop().table(AppsVersion::Table).to_owned()) 40 | .await?; 41 | Ok(()) 42 | } 43 | } 44 | 45 | fn create_apps_version_table(manager: &SchemaManager) -> TableCreateStatement { 46 | Table::create() 47 | .table(AppsVersion::Table) 48 | .col( 49 | ColumnDef::new(AppsVersion::AppsVersionId) 50 | .string() 51 | .string_len(64) 52 | .not_null() 53 | .primary_key(), 54 | ) 55 | .col( 56 | ColumnDef::new(AppsVersion::Platform) 57 | .string() 58 | .string_len(10) 59 | .null(), 60 | ) 61 | .col( 62 | ColumnDef::new(AppsVersion::PackageId) 63 | .string() 64 | .string_len(20) 65 | .null(), 66 | ) 67 | .col( 68 | ColumnDef::new(AppsVersion::CurrentVersion) 69 | .string() 70 | .string_len(10) 71 | .null(), 72 | ) 73 | .col( 74 | ColumnDef::new(AppsVersion::MinimalVersion) 75 | .string() 76 | .string_len(10) 77 | .null(), 78 | ) 79 | .col( 80 | ColumnDef::new(AppsVersion::UserId) 81 | .string() 82 | .string_len(64) 83 | .null(), 84 | ) 85 | .col( 86 | ColumnDef::new(AppsVersion::CreatedAt) 87 | .timestamp() 88 | .extra("DEFAULT CURRENT_TIMESTAMP".to_owned()), 89 | ) 90 | .col( 91 | ColumnDef::new(AppsVersion::UpdatedAt) 92 | .timestamp() 93 | .extra(on_update_current_timestamp(manager)), 94 | ) 95 | .foreign_key( 96 | ForeignKey::create() 97 | .name("FK-apps_version-user_id-user-user_id") 98 | .from(AppsVersion::Table, AppsVersion::UserId) 99 | .to(User::Table, User::UserId), 100 | ) 101 | .to_owned() 102 | } 103 | 104 | #[derive(Iden)] 105 | pub enum AppsVersion { 106 | Table, 107 | AppsVersionId, 108 | Platform, 109 | PackageId, 110 | CurrentVersion, 111 | MinimalVersion, 112 | UserId, 113 | CreatedAt, 114 | UpdatedAt, 115 | } 116 | 117 | #[derive(Iden)] 118 | enum User { 119 | Table, 120 | UserId, 121 | } 122 | -------------------------------------------------------------------------------- /libs/core/src/migration/m_20230201_000010_create_config_table.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::db_tools::on_update_current_timestamp; 2 | use sea_orm_migration::prelude::*; 3 | 4 | pub struct Migration; 5 | 6 | impl MigrationName for Migration { 7 | fn name(&self) -> &str { 8 | "m_20230201_000010_create_config_table" 9 | } 10 | } 11 | 12 | #[async_trait] 13 | impl MigrationTrait for Migration { 14 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 15 | manager.create_table(create_config_table(manager)).await?; 16 | manager.exec_stmt(seed_config_table()).await?; 17 | Ok(()) 18 | } 19 | 20 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 21 | manager 22 | .drop_table(Table::drop().table(Config::Table).to_owned()) 23 | .await?; 24 | Ok(()) 25 | } 26 | } 27 | 28 | fn seed_config_table() -> InsertStatement { 29 | Query::insert() 30 | .into_table(Config::Table) 31 | .columns([Config::ConfigKey, Config::ConfigValue]) 32 | .values_panic(["app_status".into(), "normal".into()]) 33 | .values_panic([ 34 | "app_maintaining_message".into(), 35 | DEFAULT_MAINTENANCE_MESSAGE.into(), 36 | ]) 37 | .to_owned() 38 | } 39 | 40 | fn create_config_table(manager: &SchemaManager) -> TableCreateStatement { 41 | Table::create() 42 | .table(Config::Table) 43 | .col( 44 | ColumnDef::new(Config::ConfigKey) 45 | .string() 46 | .string_len(150) 47 | .not_null(), 48 | ) 49 | .col( 50 | ColumnDef::new(Config::ConfigValue) 51 | .string() 52 | .text() 53 | .not_null(), 54 | ) 55 | .col( 56 | ColumnDef::new(Config::UserId) 57 | .string() 58 | .string_len(64) 59 | .null(), 60 | ) 61 | .col( 62 | ColumnDef::new(Config::StatusId) 63 | .string() 64 | .string_len(1) 65 | .not_null() 66 | .default("A"), 67 | ) 68 | .col( 69 | ColumnDef::new(Config::CreatedAt) 70 | .timestamp() 71 | .extra("DEFAULT CURRENT_TIMESTAMP".to_owned()), 72 | ) 73 | .col( 74 | ColumnDef::new(Config::UpdatedAt) 75 | .timestamp() 76 | .extra(on_update_current_timestamp(manager)), 77 | ) 78 | .foreign_key( 79 | ForeignKey::create() 80 | .name("FK-config-user_id-user-user_id") 81 | .from(Config::Table, Config::UserId) 82 | .to(User::Table, User::UserId), 83 | ) 84 | .to_owned() 85 | } 86 | 87 | #[derive(Iden)] 88 | pub enum Config { 89 | Table, 90 | ConfigKey, 91 | ConfigValue, 92 | UserId, 93 | StatusId, 94 | CreatedAt, 95 | UpdatedAt, 96 | } 97 | 98 | #[derive(Iden)] 99 | enum User { 100 | Table, 101 | UserId, 102 | } 103 | 104 | const DEFAULT_MAINTENANCE_MESSAGE :&str = "We are currently performing maintenance on our servers to improve their performance and stability. 105 | During this time, our services may be temporarily unavailable. 106 | We apologize for any inconvenience this may cause and appreciate your understanding as we work to improve our system."; 107 | -------------------------------------------------------------------------------- /libs/core/src/migration/mod.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | mod m_20221217_000001_create_user_table; 4 | mod m_20230111_000002_create_file_table; 5 | mod m_20230113_000003_create_category_table; 6 | mod m_20230113_000004_create_post_table; 7 | mod m_20230114_000005_create_user_role_column; 8 | mod m_20230116_000006_create_post_reply_table; 9 | mod m_20230124_000007_create_user_ban_history_table; 10 | mod m_20230130_000008_create_default_categories; 11 | mod m_20230131_000009_create_apps_version_table; 12 | mod m_20230201_000010_create_config_table; 13 | 14 | pub struct Migrator; 15 | pub use sea_orm_migration::MigratorTrait; 16 | 17 | #[async_trait] 18 | impl MigratorTrait for Migrator { 19 | fn migrations() -> Vec> { 20 | vec![ 21 | Box::new(m_20221217_000001_create_user_table::Migration), 22 | Box::new(m_20230111_000002_create_file_table::Migration), 23 | Box::new(m_20230113_000003_create_category_table::Migration), 24 | Box::new(m_20230113_000004_create_post_table::Migration), 25 | Box::new(m_20230114_000005_create_user_role_column::Migration), 26 | Box::new(m_20230116_000006_create_post_reply_table::Migration), 27 | Box::new(m_20230124_000007_create_user_ban_history_table::Migration), 28 | Box::new(m_20230130_000008_create_default_categories::Migration), 29 | Box::new(m_20230131_000009_create_apps_version_table::Migration), 30 | Box::new(m_20230201_000010_create_config_table::Migration), 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /libs/core/src/repository/constants.rs: -------------------------------------------------------------------------------- 1 | use strum_macros::{Display, EnumProperty, EnumString}; 2 | 3 | #[derive(Display, EnumString, EnumProperty)] 4 | pub enum CachingKey { 5 | #[strum(serialize = "config-categories")] 6 | CategoryConfigs, 7 | } 8 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/db_config.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::{DatabaseConnection, *}; 2 | use std::sync::Arc; 3 | 4 | use super::entities::{apps_version, category, config}; 5 | pub use super::entities::{apps_version::AppsVersion, category::Category, config::Config}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct DbConfig { 9 | db_connection: Arc, 10 | } 11 | 12 | #[async_trait] 13 | pub trait DbConfigTrait { 14 | async fn get_apps_versions(&self) -> Result, DbErr>; 15 | async fn get_categories(&self) -> Result, DbErr>; 16 | async fn get_configs(&self) -> Result, DbErr>; 17 | } 18 | 19 | impl DbConfig { 20 | pub fn new(db_connection: &Arc) -> DbConfig { 21 | DbConfig { 22 | db_connection: db_connection.clone(), 23 | } 24 | } 25 | } 26 | 27 | #[async_trait] 28 | impl DbConfigTrait for DbConfig { 29 | async fn get_configs(&self) -> Result, DbErr> { 30 | config::Entity::find().all(&*self.db_connection).await 31 | } 32 | 33 | async fn get_apps_versions(&self) -> Result, DbErr> { 34 | apps_version::Entity::find().all(&*self.db_connection).await 35 | } 36 | 37 | async fn get_categories(&self) -> Result, DbErr> { 38 | let categories = category::Entity::find() 39 | .filter(category::Column::StatusId.eq("A")) 40 | .order_by_asc(category::Column::SortIndex) 41 | .all(&*self.db_connection) 42 | .await 43 | .unwrap(); 44 | Ok(categories) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/db_thread.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use sea_orm::{DatabaseConnection, *}; 3 | use std::sync::Arc; 4 | 5 | use super::entities::{post, post_reply, user}; 6 | 7 | #[non_exhaustive] 8 | #[derive(Debug, Clone)] 9 | pub struct DbThread { 10 | db_connection: Arc, 11 | } 12 | 13 | impl DbThread { 14 | pub fn new(db_connection: &Arc) -> DbThread { 15 | DbThread { 16 | db_connection: db_connection.clone(), 17 | } 18 | } 19 | } 20 | 21 | #[async_trait] 22 | pub trait DbThreadTrait { 23 | // Post 24 | async fn get_threads(&self, input: &GetThreadsInput) -> Result, DbErr>; 25 | } 26 | 27 | #[async_trait] 28 | impl DbThreadTrait for DbThread { 29 | async fn get_threads(&self, input: &GetThreadsInput) -> Result, DbErr> { 30 | todo!() 31 | } 32 | } 33 | 34 | // 35 | 36 | pub struct GetThreadsInput { 37 | category_id: Option, 38 | last_id: Option, 39 | limit: i32, 40 | order: String, 41 | } 42 | 43 | impl Default for GetThreadsInput { 44 | fn default() -> Self { 45 | Self { 46 | category_id: None, 47 | last_id: None, 48 | limit: 60, 49 | order: "DESC".to_owned(), 50 | } 51 | } 52 | } 53 | 54 | fn filter_query_results(builder: &mut Select, input: &GetThreadsInput) { 55 | let mut builder_clone = builder.clone(); 56 | builder_clone = builder_clone.filter(user::Column::StatusId.eq("normal")); 57 | 58 | *builder = builder_clone; 59 | } 60 | 61 | #[derive(Debug, FromQueryResult)] 62 | pub struct RawThreadOutput { 63 | pub id: i32, 64 | pub quote_reply_id: Option, 65 | pub quote_reply_content: Option, 66 | pub no_of_reply: u32, 67 | pub content: String, 68 | pub like_count: i32, 69 | pub dislike_count: i32, 70 | pub low_quality: bool, 71 | pub user_id: String, 72 | pub user_role: String, 73 | pub user_name: String, 74 | pub user_email: String, 75 | pub user_avatar_url: Option, 76 | pub created_at: DateTime, 77 | pub updated_at: DateTime, 78 | } 79 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/entities/apps_version.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] 5 | #[sea_orm(table_name = "apps_version")] 6 | pub struct Model { 7 | #[sea_orm(primary_key, column_name = "apps_version_id")] 8 | pub id: String, 9 | pub platform: String, 10 | pub package_id: String, 11 | pub current_version: String, 12 | pub minimal_version: String, 13 | #[sea_orm(nullable)] 14 | pub user_id: Option, 15 | pub created_at: DateTimeUtc, 16 | pub updated_at: DateTimeUtc, 17 | } 18 | 19 | #[derive(Copy, Clone, Debug, EnumIter)] 20 | pub enum Relation { 21 | User, 22 | } 23 | 24 | impl RelationTrait for Relation { 25 | fn def(&self) -> RelationDef { 26 | match self { 27 | Relation::User => Entity::belongs_to(super::user::Entity).into(), 28 | } 29 | } 30 | } 31 | 32 | impl Related for Entity { 33 | fn to() -> RelationDef { 34 | Relation::User.def() 35 | } 36 | } 37 | 38 | impl ActiveModelBehavior for ActiveModel {} 39 | 40 | pub type AppsVersion = Model; 41 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/entities/category.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] 5 | #[sea_orm(table_name = "category")] 6 | pub struct Model { 7 | #[sea_orm(primary_key, column_name = "category_id")] 8 | pub id: String, 9 | pub name: String, 10 | pub slug: String, 11 | #[sea_orm(nullable)] 12 | pub description: Option, 13 | pub level: u32, 14 | pub postable: bool, 15 | pub sort_index: i32, 16 | pub parent_id: Option, 17 | #[sea_orm(default_value = 0)] 18 | pub count: i64, 19 | #[sea_orm(nullable)] 20 | pub user_id: Option, 21 | #[sea_orm(default_value = "A")] 22 | pub status_id: String, 23 | pub created_at: DateTimeUtc, 24 | pub updated_at: DateTimeUtc, 25 | } 26 | 27 | #[derive(Copy, Clone, Debug, EnumIter)] 28 | pub enum Relation { 29 | ParentCategory, 30 | User, 31 | } 32 | 33 | impl RelationTrait for Relation { 34 | fn def(&self) -> RelationDef { 35 | match self { 36 | Relation::ParentCategory => Entity::belongs_to(Entity).into(), 37 | Relation::User => Entity::belongs_to(super::user::Entity).into(), 38 | } 39 | } 40 | } 41 | 42 | impl Related for Entity { 43 | fn to() -> RelationDef { 44 | Relation::ParentCategory.def() 45 | } 46 | } 47 | 48 | impl Related for Entity { 49 | fn to() -> RelationDef { 50 | Relation::User.def() 51 | } 52 | } 53 | 54 | impl ActiveModelBehavior for ActiveModel {} 55 | 56 | pub type Category = Model; 57 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/entities/config.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] 5 | #[sea_orm(table_name = "config")] 6 | pub struct Model { 7 | #[sea_orm(primary_key, column_name = "config_key")] 8 | pub key: String, 9 | #[sea_orm(column_name = "config_value")] 10 | pub value: String, 11 | #[sea_orm(nullable)] 12 | pub user_id: Option, 13 | #[sea_orm(default_value = "A")] 14 | pub status_id: String, 15 | pub created_at: DateTimeUtc, 16 | pub updated_at: DateTimeUtc, 17 | } 18 | 19 | #[derive(Copy, Clone, Debug, EnumIter)] 20 | pub enum Relation { 21 | User, 22 | } 23 | 24 | impl RelationTrait for Relation { 25 | fn def(&self) -> RelationDef { 26 | match self { 27 | Relation::User => Entity::belongs_to(super::user::Entity).into(), 28 | } 29 | } 30 | } 31 | 32 | impl Related for Entity { 33 | fn to() -> RelationDef { 34 | Relation::User.def() 35 | } 36 | } 37 | 38 | impl ActiveModelBehavior for ActiveModel {} 39 | 40 | pub type Config = Model; 41 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/entities/file.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] 5 | #[sea_orm(table_name = "file")] 6 | pub struct Model { 7 | #[sea_orm(primary_key, column_name = "file_id")] 8 | pub id: String, 9 | pub name: String, 10 | #[sea_orm(nullable)] 11 | pub alternative_text: Option, 12 | #[sea_orm(nullable)] 13 | pub caption: Option, 14 | #[sea_orm(nullable)] 15 | pub description: Option, 16 | #[sea_orm(nullable)] 17 | pub mime_type: Option, 18 | #[sea_orm(nullable)] 19 | pub size: Option, 20 | pub public_uri: Option, 21 | #[sea_orm(nullable)] 22 | pub user_id: Option, 23 | #[sea_orm(default_value = "A")] 24 | pub status_id: String, 25 | pub created_at: DateTimeUtc, 26 | pub updated_at: DateTimeUtc, 27 | } 28 | 29 | #[derive(Copy, Clone, Debug, EnumIter)] 30 | pub enum Relation { 31 | User, 32 | } 33 | 34 | impl RelationTrait for Relation { 35 | fn def(&self) -> RelationDef { 36 | match self { 37 | Relation::User => Entity::belongs_to(super::user::Entity).into(), 38 | } 39 | } 40 | } 41 | 42 | impl Related for Entity { 43 | fn to() -> RelationDef { 44 | Relation::User.def() 45 | } 46 | } 47 | 48 | impl ActiveModelBehavior for ActiveModel {} 49 | 50 | pub type File = Model; 51 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/entities/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod apps_version; 2 | pub mod category; 3 | pub mod config; 4 | pub mod file; 5 | pub mod post; 6 | pub mod post_reply; 7 | pub mod prelude; 8 | pub mod user; 9 | pub mod user_ban_history; 10 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/entities/post.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] 5 | #[sea_orm(table_name = "post")] 6 | pub struct Model { 7 | #[sea_orm(primary_key, auto_increment = true, column_name = "post_id")] 8 | pub id: i32, 9 | pub title: String, 10 | pub slug: String, 11 | pub category_id: String, 12 | pub comment_count: i32, 13 | pub max_comment_count: i32, 14 | pub content: String, 15 | pub user_id: String, 16 | pub status_id: String, 17 | pub created_at: DateTimeUtc, 18 | pub updated_at: DateTimeUtc, 19 | } 20 | 21 | #[derive(Copy, Clone, Debug, EnumIter)] 22 | pub enum Relation { 23 | PostReply, 24 | } 25 | 26 | impl RelationTrait for Relation { 27 | fn def(&self) -> RelationDef { 28 | match self { 29 | Relation::PostReply => Entity::has_many(super::post_reply::Entity).into(), 30 | } 31 | } 32 | } 33 | 34 | impl Related for Entity { 35 | fn to() -> RelationDef { 36 | Relation::PostReply.def() 37 | } 38 | } 39 | 40 | impl ActiveModelBehavior for ActiveModel {} 41 | 42 | pub type Post = Model; 43 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/entities/post_reply.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] 5 | #[sea_orm(table_name = "post_reply")] 6 | pub struct Model { 7 | #[sea_orm(primary_key, column_name = "post_reply_id")] 8 | pub id: i32, 9 | pub no_of_reply: u32, 10 | pub quote_reply_id: Option, 11 | pub content: String, 12 | pub post_id: i32, 13 | pub like_count: i32, 14 | pub dislike_count: i32, 15 | pub low_quality: bool, 16 | pub user_id: String, 17 | #[sea_orm(default_value = "A")] 18 | pub status_id: String, 19 | pub created_at: DateTimeUtc, 20 | pub updated_at: DateTimeUtc, 21 | } 22 | 23 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 24 | pub enum Relation { 25 | #[sea_orm( 26 | belongs_to = "Entity", 27 | from = "Column::QuoteReplyId", 28 | to = "Column::Id" 29 | )] 30 | QuoteReply, 31 | #[sea_orm( 32 | belongs_to = "super::post::Entity", 33 | from = "Column::PostId", 34 | to = "super::post::Column::Id" 35 | )] 36 | Post, 37 | #[sea_orm( 38 | belongs_to = "super::user::Entity", 39 | from = "Column::UserId", 40 | to = "super::user::Column::Id" 41 | )] 42 | User, 43 | } 44 | 45 | impl Related for Entity { 46 | fn to() -> RelationDef { 47 | Relation::QuoteReply.def() 48 | } 49 | } 50 | 51 | impl Related for Entity { 52 | fn to() -> RelationDef { 53 | Relation::Post.def() 54 | } 55 | } 56 | 57 | impl Related for Entity { 58 | fn to() -> RelationDef { 59 | Relation::User.def() 60 | } 61 | } 62 | 63 | impl ActiveModelBehavior for ActiveModel {} 64 | 65 | pub type PostReply = Model; 66 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/entities/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use super::category::Entity as Category; 2 | pub use super::file::Entity as File; 3 | pub use super::post::Entity as Post; 4 | pub use super::post_reply::Entity as PostReply; 5 | pub use super::user::Entity as User; 6 | pub use super::user_ban_history::Entity as UserBanHistory; 7 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/entities/user.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] 5 | #[sea_orm(table_name = "user")] 6 | pub struct Model { 7 | #[sea_orm(primary_key, column_name = "user_id")] 8 | pub id: String, 9 | #[sea_orm(unique)] 10 | pub sub: String, 11 | pub role: String, 12 | pub name: String, 13 | #[sea_orm(unique)] 14 | pub email: String, 15 | #[sea_orm(nullable)] 16 | pub avatar_url: Option, 17 | #[sea_orm(nullable)] 18 | pub notes: Option, 19 | pub no_of_ban: u32, 20 | #[sea_orm(default_value = "normal")] 21 | pub status_id: String, 22 | pub created_at: DateTimeUtc, 23 | pub updated_at: DateTimeUtc, 24 | } 25 | 26 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 27 | pub enum Relation { 28 | #[sea_orm(has_many = "super::category::Entity")] 29 | Category, 30 | #[sea_orm(has_many = "super::file::Entity")] 31 | File, 32 | #[sea_orm(has_many = "super::post_reply::Entity")] 33 | PostReply, 34 | #[sea_orm(has_many = "super::user_ban_history::Entity")] 35 | BanUserId, 36 | } 37 | 38 | impl Related for Entity { 39 | fn to() -> RelationDef { 40 | Relation::Category.def() 41 | } 42 | } 43 | 44 | impl Related for Entity { 45 | fn to() -> RelationDef { 46 | Relation::File.def() 47 | } 48 | } 49 | 50 | impl Related for Entity { 51 | fn to() -> RelationDef { 52 | Relation::PostReply.def() 53 | } 54 | } 55 | 56 | impl Related for Entity { 57 | fn to() -> RelationDef { 58 | Relation::BanUserId.def() 59 | } 60 | } 61 | 62 | impl ActiveModelBehavior for ActiveModel {} 63 | 64 | pub type User = Model; 65 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/entities/user_ban_history.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] 5 | #[sea_orm(table_name = "user_ban_history")] 6 | pub struct Model { 7 | #[sea_orm( 8 | primary_key, 9 | auto_increment = true, 10 | column_name = "user_ban_history_id" 11 | )] 12 | pub id: i32, 13 | pub ban_user_id: String, 14 | pub ban_reason: Option, 15 | pub ban_time: Option, 16 | pub release_time: Option, 17 | pub user_id: String, 18 | #[sea_orm(default_value = "A")] 19 | pub status_id: String, 20 | pub created_at: DateTimeUtc, 21 | pub updated_at: DateTimeUtc, 22 | } 23 | 24 | #[derive(Copy, Clone, Debug, EnumIter)] 25 | pub enum Relation { 26 | BanUserId, 27 | User, 28 | } 29 | 30 | impl RelationTrait for Relation { 31 | fn def(&self) -> RelationDef { 32 | match self { 33 | Relation::BanUserId => Entity::belongs_to(super::user::Entity).into(), 34 | Relation::User => Entity::belongs_to(super::user::Entity).into(), 35 | } 36 | } 37 | } 38 | 39 | impl Related for Entity { 40 | fn to() -> RelationDef { 41 | Relation::User.def() 42 | } 43 | } 44 | 45 | impl ActiveModelBehavior for ActiveModel {} 46 | 47 | pub type UserBanHistory = Model; 48 | -------------------------------------------------------------------------------- /libs/core/src/repository/database/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db_category; 2 | 3 | pub mod db_config; 4 | pub mod db_file; 5 | pub mod db_post; 6 | // pub mod db_thread; 7 | pub mod db_user; 8 | pub mod entities; 9 | -------------------------------------------------------------------------------- /libs/core/src/repository/errors.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Display, Error}; 2 | 3 | #[derive(Debug, Error, Display)] 4 | pub enum RepoError { 5 | #[display(fmt = "Repo Post Category Error: Generic")] 6 | Generic, 7 | } 8 | -------------------------------------------------------------------------------- /libs/core/src/repository/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod database; 3 | pub mod errors; 4 | 5 | pub mod repo_category; 6 | pub mod repo_config; 7 | pub mod repo_file; 8 | pub mod repo_post; 9 | pub mod repo_user; 10 | -------------------------------------------------------------------------------- /libs/core/src/repository/repo_category.rs: -------------------------------------------------------------------------------- 1 | pub use crate::repository::{ 2 | database::db_category::{ 3 | Category, CategoryFilter, CreateCategoryInput, DbCategory, DbCategoryTrait, 4 | ListCategoryInput, UpdateCategoryInput, 5 | }, 6 | errors::RepoError, 7 | }; 8 | use error_stack::{IntoReport, Result, ResultExt}; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct RepoCategory { 12 | category: DbCategory, 13 | } 14 | 15 | impl RepoCategory { 16 | pub fn new(category: DbCategory) -> RepoCategory { 17 | RepoCategory { category } 18 | } 19 | } 20 | 21 | #[async_trait] 22 | pub trait RepoCategoryTrait { 23 | async fn list(&self, input: &ListCategoryInput) -> Result, RepoError>; 24 | async fn count(&self, &filter: &CategoryFilter) -> Result; 25 | async fn find_by_id(&self, id: &str) -> Result, RepoError>; 26 | async fn find_by_slug(&self, slug: &str) -> Result, RepoError>; 27 | async fn create(&self, input: &CreateCategoryInput) -> Result; 28 | async fn update(&self, input: &UpdateCategoryInput) -> Result; 29 | async fn delete(&self, id: &str) -> Result<(), RepoError>; 30 | } 31 | 32 | #[async_trait] 33 | impl RepoCategoryTrait for RepoCategory { 34 | async fn list(&self, input: &ListCategoryInput) -> Result, RepoError> { 35 | self.category 36 | .list(input) 37 | .await 38 | .into_report() 39 | .change_context(RepoError::Generic) 40 | } 41 | 42 | async fn count(&self, filter: &CategoryFilter) -> Result { 43 | self.category 44 | .count(filter) 45 | .await 46 | .into_report() 47 | .change_context(RepoError::Generic) 48 | } 49 | 50 | async fn find_by_id(&self, id: &str) -> Result, RepoError> { 51 | self.category 52 | .find_by_id(id) 53 | .await 54 | .into_report() 55 | .change_context(RepoError::Generic) 56 | } 57 | 58 | async fn find_by_slug(&self, slug: &str) -> Result, RepoError> { 59 | self.category 60 | .find_by_slug(slug) 61 | .await 62 | .into_report() 63 | .change_context(RepoError::Generic) 64 | } 65 | 66 | async fn create(&self, input: &CreateCategoryInput) -> Result { 67 | self.category 68 | .create(input) 69 | .await 70 | .into_report() 71 | .change_context(RepoError::Generic) 72 | } 73 | 74 | async fn update(&self, input: &UpdateCategoryInput) -> Result { 75 | self.category 76 | .update(input) 77 | .await 78 | .into_report() 79 | .change_context(RepoError::Generic) 80 | } 81 | 82 | async fn delete(&self, id: &str) -> Result<(), RepoError> { 83 | self.category 84 | .delete(id) 85 | .await 86 | .into_report() 87 | .change_context(RepoError::Generic)?; 88 | Ok(()) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /libs/core/src/repository/repo_config.rs: -------------------------------------------------------------------------------- 1 | use crate::repository::constants; 2 | pub use crate::repository::{ 3 | database::db_config::{AppsVersion, Category, Config, DbConfig, DbConfigTrait}, 4 | errors::RepoError, 5 | }; 6 | use discuz_utils::redis::{get_cached_result, set_cached_result}; 7 | use error_stack::{IntoReport, Result, ResultExt}; 8 | use fred::types::Expiration; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct RepoConfig { 12 | db_config: DbConfig, 13 | } 14 | 15 | impl RepoConfig { 16 | pub fn new(db_config: DbConfig) -> RepoConfig { 17 | RepoConfig { db_config } 18 | } 19 | } 20 | 21 | #[async_trait] 22 | pub trait RepoConfigTrait { 23 | async fn get_apps_versions(&self) -> Result, RepoError>; 24 | async fn get_categories(&self) -> Result, RepoError>; 25 | async fn get_configs(&self) -> Result, RepoError>; 26 | } 27 | 28 | #[async_trait] 29 | impl RepoConfigTrait for RepoConfig { 30 | async fn get_apps_versions(&self) -> Result, RepoError> { 31 | self.db_config 32 | .get_apps_versions() 33 | .await 34 | .into_report() 35 | .change_context(RepoError::Generic) 36 | } 37 | 38 | async fn get_categories(&self) -> Result, RepoError> { 39 | let cache_key = constants::CachingKey::CategoryConfigs.to_string(); 40 | if let Some(categories) = get_cached_result::>(&cache_key).await { 41 | return Ok(categories); 42 | } 43 | let categories = self 44 | .db_config 45 | .get_categories() 46 | .await 47 | .into_report() 48 | .change_context(RepoError::Generic)?; 49 | set_cached_result(&cache_key, &categories, Some(Expiration::EX(60))).await; 50 | Ok(categories) 51 | } 52 | 53 | async fn get_configs(&self) -> Result, RepoError> { 54 | self.db_config 55 | .get_configs() 56 | .await 57 | .into_report() 58 | .change_context(RepoError::Generic) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /libs/core/src/repository/repo_file.rs: -------------------------------------------------------------------------------- 1 | pub use crate::repository::{ 2 | database::db_file::{CreateFileInput, DbFile, DbFileTrait, File, UpdateFileInput}, 3 | errors::RepoError, 4 | }; 5 | use error_stack::{IntoReport, Result, ResultExt}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct RepoFile { 9 | db_file: DbFile, 10 | } 11 | 12 | impl RepoFile { 13 | pub fn new(db_file: DbFile) -> RepoFile { 14 | RepoFile { db_file } 15 | } 16 | } 17 | 18 | #[async_trait] 19 | pub trait RepoFileTrait { 20 | async fn create(&self, input: &CreateFileInput) -> Result; 21 | async fn update(&self, input: &UpdateFileInput) -> Result<(), RepoError>; 22 | async fn update_status(&self, id: &str, status: &str) -> Result<(), RepoError>; 23 | async fn find_by_id(&self, id: &str) -> Result, RepoError>; 24 | } 25 | 26 | #[async_trait] 27 | impl RepoFileTrait for RepoFile { 28 | async fn create(&self, input: &CreateFileInput) -> Result { 29 | self.db_file 30 | .create(input) 31 | .await 32 | .into_report() 33 | .change_context(RepoError::Generic) 34 | } 35 | 36 | async fn update(&self, input: &UpdateFileInput) -> Result<(), RepoError> { 37 | self.db_file 38 | .update(input) 39 | .await 40 | .into_report() 41 | .change_context(RepoError::Generic) 42 | } 43 | 44 | async fn update_status(&self, id: &str, status: &str) -> Result<(), RepoError> { 45 | self.db_file 46 | .update_status(id, status) 47 | .await 48 | .into_report() 49 | .change_context(RepoError::Generic) 50 | } 51 | 52 | async fn find_by_id(&self, id: &str) -> Result, RepoError> { 53 | self.db_file 54 | .find_by_id(id) 55 | .await 56 | .into_report() 57 | .change_context(RepoError::Generic) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /libs/core/src/repository/repo_post.rs: -------------------------------------------------------------------------------- 1 | pub use crate::repository::{ 2 | database::db_post::{ 3 | CreatePostInput, CreateReplyInput, DbPost, DbPostTrait, Post, PostReply, UpdatePostInput, 4 | }, 5 | errors::RepoError, 6 | }; 7 | use error_stack::{IntoReport, Result, ResultExt}; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct RepoPost { 11 | db_post: DbPost, 12 | } 13 | 14 | impl RepoPost { 15 | pub fn new(db_post: DbPost) -> RepoPost { 16 | RepoPost { db_post } 17 | } 18 | } 19 | 20 | #[async_trait] 21 | pub trait RepoPostTrait { 22 | // Post 23 | async fn find_by_id(&self, id: i32) -> Result, RepoError>; 24 | async fn create(&self, input: &CreatePostInput) -> Result; 25 | async fn update(&self, input: &UpdatePostInput) -> Result<(), RepoError>; 26 | async fn delete(&self, id: i32) -> Result<(), RepoError>; 27 | // Post Reply 28 | async fn find_reply_by_id(&self, id: i32) -> Result, RepoError>; 29 | async fn create_reply(&self, input: &CreateReplyInput) -> Result; 30 | async fn delete_reply(&self, id: i32) -> Result<(), RepoError>; 31 | } 32 | 33 | #[async_trait] 34 | impl RepoPostTrait for RepoPost { 35 | // Post 36 | async fn find_by_id(&self, id: i32) -> Result, RepoError> { 37 | self.db_post 38 | .find_by_id(id) 39 | .await 40 | .into_report() 41 | .change_context(RepoError::Generic) 42 | } 43 | 44 | async fn create(&self, input: &CreatePostInput) -> Result { 45 | self.db_post 46 | .create(input) 47 | .await 48 | .into_report() 49 | .change_context(RepoError::Generic) 50 | } 51 | 52 | async fn update(&self, input: &UpdatePostInput) -> Result<(), RepoError> { 53 | self.db_post 54 | .update(input) 55 | .await 56 | .into_report() 57 | .change_context(RepoError::Generic) 58 | } 59 | 60 | async fn delete(&self, id: i32) -> Result<(), RepoError> { 61 | self.db_post 62 | .delete(id) 63 | .await 64 | .into_report() 65 | .change_context(RepoError::Generic) 66 | } 67 | 68 | // 69 | 70 | async fn find_reply_by_id(&self, id: i32) -> Result, RepoError> { 71 | self.db_post 72 | .find_reply_by_id(id) 73 | .await 74 | .into_report() 75 | .change_context(RepoError::Generic) 76 | } 77 | 78 | async fn create_reply(&self, input: &CreateReplyInput) -> Result { 79 | self.db_post 80 | .create_reply(input) 81 | .await 82 | .into_report() 83 | .change_context(RepoError::Generic) 84 | } 85 | 86 | async fn delete_reply(&self, id: i32) -> Result<(), RepoError> { 87 | self.db_post 88 | .delete_reply(id) 89 | .await 90 | .into_report() 91 | .change_context(RepoError::Generic) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/auth_service.rs: -------------------------------------------------------------------------------- 1 | pub use crate::service::auth::utils::AuthUser; 2 | use crate::service::{ 3 | auth::{ 4 | constants::UserRole, 5 | errors::AuthError, 6 | provider::{api_provider::ApiCognitoTrait, utils::get_tokens::GetTokensOutput}, 7 | utils::{ 8 | get_auth_user_by_access_token, 9 | mock_data::{ 10 | get_fake_sub, get_mock_auth_user, should_return_mock_user_by_access_token, 11 | }, 12 | validate_user, 13 | }, 14 | }, 15 | user::user_service::User, 16 | }; 17 | 18 | use error_stack::{Result, ResultExt}; 19 | use std::{fmt::Debug, sync::Arc}; 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct AuthService { 23 | pub api_provider: Arc, 24 | } 25 | 26 | #[async_trait] 27 | pub trait AuthServiceTrait: Sync + Send + Debug { 28 | async fn validate_access_token(&self, access_token: &str) -> Result; 29 | async fn get_auth_user_by_access_token( 30 | &self, 31 | access_token: &str, 32 | ) -> Result; 33 | async fn get_tokens(&self, code: &str) -> Result; 34 | fn validate_user(&self, user: &User, roles: Option<&[UserRole]>) -> Result<(), AuthError> { 35 | validate_user::execute(user, roles)?; 36 | Ok(()) 37 | } 38 | } 39 | 40 | #[async_trait] 41 | impl AuthServiceTrait for AuthService { 42 | async fn validate_access_token(&self, access_token: &str) -> Result { 43 | if should_return_mock_user_by_access_token(access_token) { 44 | return Ok(get_fake_sub()); 45 | } 46 | self.api_provider 47 | .validate_access_token(access_token) 48 | .await 49 | .change_context(AuthError::InvalidAccessTokenError) 50 | } 51 | 52 | async fn get_auth_user_by_access_token( 53 | &self, 54 | access_token: &str, 55 | ) -> Result { 56 | if should_return_mock_user_by_access_token(access_token) { 57 | return Ok(get_mock_auth_user()); 58 | } 59 | get_auth_user_by_access_token(&*self.api_provider, access_token).await 60 | } 61 | 62 | async fn get_tokens(&self, code: &str) -> Result { 63 | self.api_provider 64 | .get_tokens(code) 65 | .await 66 | .change_context(AuthError::Generic("Invalid auth code".to_string())) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/constants.rs: -------------------------------------------------------------------------------- 1 | use strum_macros::{Display, EnumString}; 2 | 3 | #[derive(Display, EnumString, Debug, PartialEq, Eq)] 4 | pub enum UserRole { 5 | #[strum(serialize = "admin")] 6 | Admin, 7 | #[strum(serialize = "user")] 8 | User, 9 | } 10 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/errors.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use derive_more::{Display, Error}; 3 | 4 | #[derive(Debug, Display, Error, Clone)] 5 | pub enum AuthError { 6 | #[display(fmt = "{_0}")] 7 | Generic(#[error(not(source))] String), 8 | #[display(fmt = "Invalid access token")] 9 | InvalidAccessTokenError, 10 | #[display(fmt = "Insufficient privileges")] 11 | InsufficientPrivilegesError, 12 | #[display(fmt = "User banned")] 13 | UserBannedError { 14 | reason: Option, 15 | ban_time: Option, 16 | release_time: Option>, 17 | }, 18 | #[display(fmt = "Internal server error")] 19 | InternalServerError, 20 | } 21 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth_service; 2 | pub mod constants; 3 | pub mod errors; 4 | pub mod provider; 5 | pub mod utils; 6 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/provider/api_provider.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, sync::Arc}; 2 | 3 | use aws_config::SdkConfig; 4 | use aws_sdk_cognitoidentityprovider::{output::GetUserOutput, Client}; 5 | use discuz_utils::config::{get_config, Amazon, Cognito}; 6 | use error_stack::Result; 7 | 8 | use crate::service::auth::provider::{ 9 | errors::ProviderError, 10 | utils::{get_tokens, get_user_by_access_token, validate_access_token}, 11 | }; 12 | 13 | pub use get_tokens::GetTokensOutput; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct ApiCognito { 17 | pub domain: String, 18 | pub user_pool_id: String, 19 | pub client_id: String, 20 | pub redirect_uri: String, 21 | pub client: Client, 22 | } 23 | 24 | impl ApiCognito { 25 | pub fn new(sdk_config: &Arc) -> ApiCognito { 26 | let config = get_config().clone(); 27 | let client = Client::new(sdk_config); 28 | let Cognito { 29 | user_pool_id, 30 | domain, 31 | client_id, 32 | redirect_uri, 33 | } = config.amazon.cognito; 34 | ApiCognito { 35 | domain, 36 | user_pool_id, 37 | client_id, 38 | redirect_uri, 39 | client, 40 | } 41 | } 42 | } 43 | 44 | #[async_trait] 45 | pub trait ApiCognitoTrait: Sync + Send + Debug { 46 | async fn get_user_by_access_token( 47 | &self, 48 | access_token: &str, 49 | ) -> Result; 50 | async fn validate_access_token(&self, access_token: &str) -> Result; 51 | async fn get_tokens(&self, code: &str) -> Result; 52 | } 53 | 54 | #[async_trait] 55 | impl ApiCognitoTrait for ApiCognito { 56 | async fn get_user_by_access_token( 57 | &self, 58 | access_token: &str, 59 | ) -> Result { 60 | get_user_by_access_token::execute(&self.client, access_token).await 61 | } 62 | async fn validate_access_token(&self, access_token: &str) -> Result { 63 | let config = get_config(); 64 | let Amazon { region, .. } = config.amazon.clone(); 65 | validate_access_token::execute(®ion, &self.user_pool_id, access_token).await 66 | } 67 | async fn get_tokens(&self, code: &str) -> Result { 68 | get_tokens::execute(&self.domain, &self.client_id, &self.redirect_uri, code).await 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/provider/errors.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Display, Error}; 2 | 3 | #[derive(Debug, Display, Error)] 4 | pub enum ProviderError { 5 | #[display(fmt = "Provider Error: Generic {_0}")] 6 | Generic(#[error(not(source))] String), 7 | #[display(fmt = "Provider Error: Invalid Credentials")] 8 | InvalidCredentials, 9 | #[display(fmt = "Provider Error: Invalid User Code")] 10 | InvalidUserCode, 11 | #[display(fmt = "Provider Error: Invalid Access Token")] 12 | InvalidAccessToken, 13 | } 14 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/provider/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api_provider; 2 | pub mod errors; 3 | pub mod utils; 4 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/provider/utils/get_tokens.rs: -------------------------------------------------------------------------------- 1 | use error_stack::{IntoReport, Result, ResultExt}; 2 | use reqwest; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::super::errors::ProviderError; 6 | 7 | pub async fn execute( 8 | domain: &str, 9 | client_id: &str, 10 | redirect_uri: &str, 11 | code: &str, 12 | ) -> Result { 13 | let params = vec![ 14 | ("grant_type", "authorization_code"), 15 | ("client_id", client_id), 16 | ("code", code), 17 | ("redirect_uri", redirect_uri), 18 | ]; 19 | 20 | let url = &format!("https://{domain}/oauth2/token"); 21 | 22 | let response: GetTokensOutput = reqwest::Client::new() 23 | .post(url) 24 | .form(¶ms) 25 | .send() 26 | .await 27 | .into_report() 28 | .change_context(ProviderError::Generic("Invalid auth code".to_string()))? 29 | .json() 30 | .await 31 | .into_report() 32 | .change_context(ProviderError::Generic("Unexpected error".to_string()))?; 33 | 34 | Ok(response) 35 | } 36 | 37 | #[derive(Debug, Serialize, Deserialize)] 38 | pub struct GetTokensOutput { 39 | pub id_token: String, 40 | pub access_token: String, 41 | pub refresh_token: String, 42 | pub expires_in: u32, 43 | pub token_type: String, 44 | } 45 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/provider/utils/get_user_by_access_token.rs: -------------------------------------------------------------------------------- 1 | use aws_sdk_cognitoidentityprovider::{output::GetUserOutput, Client}; 2 | use error_stack::{IntoReport, Result, ResultExt}; 3 | 4 | use crate::service::auth::provider::errors::ProviderError; 5 | 6 | pub async fn execute(client: &Client, access_token: &str) -> Result { 7 | client 8 | .get_user() 9 | .access_token(access_token) 10 | .send() 11 | .await 12 | .into_report() 13 | .change_context(ProviderError::InvalidAccessToken) 14 | .attach_printable( 15 | "Unable to retrieve the user context from the Cognito by the access token", 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/provider/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_tokens; 2 | pub mod get_user_by_access_token; 3 | pub mod validate_access_token; 4 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/utils/auth_user.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct AuthUser { 3 | pub sub: String, 4 | pub username: String, 5 | pub name: String, 6 | pub email: String, 7 | pub picture: Option, 8 | pub email_verified: Option, 9 | pub phone_number_verified: Option, 10 | } 11 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/utils/get_auth_user_by_access_token.rs: -------------------------------------------------------------------------------- 1 | use std::slice::Iter; 2 | 3 | use aws_sdk_cognitoidentityprovider::model::AttributeType; 4 | use error_stack::{Report, Result, ResultExt}; 5 | 6 | use crate::service::auth::{ 7 | auth_service::AuthUser, errors::AuthError, provider::api_provider::ApiCognitoTrait, 8 | }; 9 | 10 | pub async fn get_auth_user_by_access_token( 11 | api_provider: &dyn ApiCognitoTrait, 12 | access_token: &str, 13 | ) -> Result { 14 | let user = api_provider 15 | .get_user_by_access_token(access_token) 16 | .await 17 | .change_context(AuthError::Generic( 18 | "Unable to retrieve user context by the access token".to_owned(), 19 | ))?; 20 | let attributes = match user.user_attributes() { 21 | Some(attributes) => attributes.iter(), 22 | None => { 23 | return Err(Report::new(AuthError::Generic( 24 | "Unable to extract attributes".to_owned(), 25 | ))); 26 | } 27 | }; 28 | let response = AuthUser { 29 | username: user.username().unwrap().to_owned(), 30 | sub: get_value_by_key(&attributes, "sub").unwrap(), 31 | email_verified: get_value_by_key(&attributes, "email_verified") 32 | .map(|email_verified| email_verified == "true"), 33 | name: get_value_by_key(&attributes, "name").unwrap(), 34 | phone_number_verified: get_value_by_key(&attributes, "phone_number_verified") 35 | .map(|phone_number| phone_number == "true"), 36 | email: get_value_by_key(&attributes, "email").unwrap(), 37 | picture: get_value_by_key(&attributes, "picture"), 38 | }; 39 | Ok(response) 40 | } 41 | 42 | fn get_value_by_key(attributes: &Iter, key: &str) -> Option { 43 | if let Some(attribute) = attributes.to_owned().find(|x| { 44 | if let Some(name) = x.name() { 45 | name == key 46 | } else { 47 | false 48 | } 49 | }) { 50 | if let Some(value) = attribute.value() { 51 | return Some(value.to_owned()); 52 | } 53 | } 54 | None 55 | } 56 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/utils/mock_data.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | constants::{FAKE_ACCESS_TOKEN, FAKE_SUB}, 3 | service::auth::utils::AuthUser, 4 | }; 5 | use discuz_utils::config::get_config; 6 | 7 | pub fn should_return_mock_user_by_access_token(access_token: &str) -> bool { 8 | let is_development_mode = !get_config().is_production(); 9 | access_token == FAKE_ACCESS_TOKEN && is_development_mode 10 | } 11 | 12 | pub fn get_fake_sub() -> String { 13 | FAKE_SUB.to_owned() 14 | } 15 | 16 | pub fn get_mock_auth_user() -> AuthUser { 17 | AuthUser { 18 | sub: FAKE_SUB.to_owned(), 19 | username: FAKE_SUB.to_owned(), 20 | name: "Fake User".to_owned(), 21 | email: "fake@user.com".to_owned(), 22 | picture: None, 23 | email_verified: None, 24 | phone_number_verified: None, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod auth_user; 2 | mod get_auth_user_by_access_token; 3 | pub mod mock_data; 4 | 5 | pub use auth_user::AuthUser; 6 | pub use get_auth_user_by_access_token::get_auth_user_by_access_token; 7 | pub mod validate_permission; 8 | pub mod validate_user; 9 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/utils/validate_permission.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::service::{ 4 | auth::{constants::UserRole, errors::AuthError}, 5 | user::user_service::User, 6 | }; 7 | 8 | pub fn execute(user: &User, roles: &[UserRole]) -> Result<(), AuthError> { 9 | let user_role = UserRole::from_str(&user.role).map_err(|_| AuthError::InternalServerError)?; 10 | 11 | if !roles.iter().any(|role| *role == user_role) { 12 | return Err(AuthError::InsufficientPrivilegesError); 13 | } 14 | 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /libs/core/src/service/auth/utils/validate_user.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::service::{ 4 | auth::{constants::UserRole, errors::AuthError, utils::validate_permission}, 5 | user::{constants::UserStatus, user_service::User}, 6 | }; 7 | 8 | pub fn execute(user: &User, roles: Option<&[UserRole]>) -> Result<(), AuthError> { 9 | if let Some(roles) = roles { 10 | validate_permission::execute(user, roles)?; 11 | } 12 | 13 | let user_status = UserStatus::from_str(&user.status_id.to_owned()) 14 | .map_err(|_| AuthError::InternalServerError)?; 15 | 16 | match user_status { 17 | UserStatus::Normal => Ok(()), 18 | // TODO: fetch the ban data from database 19 | UserStatus::Banned => Err(AuthError::UserBannedError { 20 | reason: Some( 21 | "Your account has been banned by the admin because of violating our terms of service" 22 | .to_owned(), 23 | ), 24 | ban_time: None, 25 | release_time: None, 26 | }), 27 | UserStatus::Deactivated => Err(AuthError::UserBannedError { 28 | reason: Some( 29 | "Your account has been deleted by the admin for violating our terms of service" 30 | .to_owned(), 31 | ), 32 | ban_time: None, 33 | release_time: None, 34 | }), 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libs/core/src/service/category/category_service.rs: -------------------------------------------------------------------------------- 1 | use error_stack::Result; 2 | use std::fmt::Debug; 3 | 4 | pub use crate::{ 5 | repository::repo_category::Category, 6 | service::category::utils::{ 7 | create::CreateCategoryInput, 8 | get_categories::{GetCategoriesResponse, ListCategoryInput}, 9 | update::UpdateCategoryInput, 10 | }, 11 | }; 12 | 13 | use crate::{ 14 | repository::repo_category::RepoCategory, 15 | service::category::{errors::CategoryError, utils}, 16 | }; 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct CategoryService { 20 | pub repo_category: RepoCategory, 21 | } 22 | 23 | #[async_trait] 24 | pub trait CategoryServiceTrait: Sync + Send + Debug { 25 | async fn get_categories( 26 | &self, 27 | input: Option<&ListCategoryInput>, 28 | ) -> Result; 29 | async fn find_by_id(&self, id: &str) -> Result, CategoryError>; 30 | async fn find_by_slug(&self, slug: &str) -> Result, CategoryError>; 31 | async fn create(&self, input: &CreateCategoryInput) -> Result; 32 | async fn update(&self, input: &UpdateCategoryInput) -> Result; 33 | async fn delete(&self, id: &str) -> Result<(), CategoryError>; 34 | } 35 | 36 | #[async_trait] 37 | impl CategoryServiceTrait for CategoryService { 38 | async fn get_categories( 39 | &self, 40 | input: Option<&ListCategoryInput>, 41 | ) -> Result { 42 | utils::get_categories::execute(&self.repo_category, input).await 43 | } 44 | 45 | async fn find_by_id(&self, id: &str) -> Result, CategoryError> { 46 | utils::find_by_id::execute(&self.repo_category, id).await 47 | } 48 | 49 | async fn find_by_slug(&self, slug: &str) -> Result, CategoryError> { 50 | utils::find_by_slug::execute(&self.repo_category, slug).await 51 | } 52 | 53 | async fn create(&self, input: &CreateCategoryInput) -> Result { 54 | utils::create::execute(&self.repo_category, input).await 55 | } 56 | 57 | async fn update(&self, input: &UpdateCategoryInput) -> Result { 58 | utils::update::execute(&self.repo_category, input).await 59 | } 60 | 61 | async fn delete(&self, id: &str) -> Result<(), CategoryError> { 62 | utils::delete::execute(&self.repo_category, id).await 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /libs/core/src/service/category/errors.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Display, Error}; 2 | use strum_macros::EnumProperty; 3 | 4 | #[derive(Debug, Display, EnumProperty, Error)] 5 | pub enum CategoryError { 6 | #[display(fmt = "Post Category Error: Generic {_0}")] 7 | #[strum(props(code = "generic"))] 8 | Generic(#[error(not(source))] String), 9 | #[display(fmt = "Post Category Error: Target category not exist")] 10 | #[strum(props(code = "category_not_exist"))] 11 | CategoryNotExistError, 12 | #[display( 13 | fmt = "Post Category Error: Can't create or update the category because the target category name exists" 14 | )] 15 | #[strum(props(code = "duplicate_category"))] 16 | DuplicateCategoryError { 17 | name: String, 18 | detail: Option, 19 | }, 20 | #[display(fmt = "Post Category Error: Internal Server Error")] 21 | #[strum(props(code = "internal_server_error"))] 22 | InternalServerError, 23 | } 24 | -------------------------------------------------------------------------------- /libs/core/src/service/category/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod category_service; 2 | pub mod errors; 3 | pub mod utils; 4 | pub use category_service::Category; 5 | -------------------------------------------------------------------------------- /libs/core/src/service/category/utils/create.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_category::{self, Category, RepoCategory, RepoCategoryTrait}, 3 | service::category::{ 4 | errors::CategoryError, 5 | utils::update::{execute as updateCategory, UpdateCategoryInput}, 6 | }, 7 | }; 8 | use error_stack::{Result, ResultExt}; 9 | use slugify::slugify; 10 | 11 | pub async fn execute( 12 | repo_category: &RepoCategory, 13 | input: &CreateCategoryInput, 14 | ) -> Result { 15 | let slug = slugify!(&input.name); 16 | let category = repo_category 17 | .find_by_slug(&slug) 18 | .await 19 | .change_context(CategoryError::InternalServerError)?; 20 | 21 | if let Some(category) = category { 22 | if category.status_id == "D" { 23 | let category = update_deleted_category(repo_category, &category.id, input).await?; 24 | Ok(category) 25 | } else { 26 | let error = CategoryError::DuplicateCategoryError { 27 | name: input.name.to_owned(), 28 | detail: Some(format!("The post category {} already exists", &input.name)), 29 | }; 30 | Err(error.into()) 31 | } 32 | } else { 33 | let category = create_category(repo_category, input).await?; 34 | Ok(category) 35 | } 36 | } 37 | 38 | pub async fn create_category( 39 | repo_category: &RepoCategory, 40 | input: &CreateCategoryInput, 41 | ) -> Result { 42 | let input = repo_category::CreateCategoryInput { 43 | name: input.name.to_owned(), 44 | slug: slugify!(&input.name), 45 | parent_id: input.parent_id.to_owned(), 46 | description: input.description.to_owned(), 47 | postable: input.postable.to_owned(), 48 | sort_index: input.sort_index.to_owned(), 49 | level: input.level.to_owned(), 50 | user_id: input.user_id.to_owned(), 51 | }; 52 | let id = repo_category 53 | .create(&input) 54 | .await 55 | .change_context(CategoryError::InternalServerError)?; 56 | 57 | let category = repo_category 58 | .find_by_id(&id) 59 | .await 60 | .change_context(CategoryError::InternalServerError)? 61 | .ok_or(CategoryError::InternalServerError)?; 62 | 63 | Ok(category) 64 | } 65 | 66 | pub async fn update_deleted_category( 67 | repo_category: &RepoCategory, 68 | id: &str, 69 | input: &CreateCategoryInput, 70 | ) -> Result { 71 | let CreateCategoryInput { 72 | name, 73 | description, 74 | parent_id, 75 | user_id, 76 | postable, 77 | sort_index, 78 | level, 79 | } = input; 80 | 81 | let input = UpdateCategoryInput { 82 | id: id.to_owned(), 83 | name: name.to_owned(), 84 | description: description.to_owned(), 85 | postable: postable.to_owned(), 86 | level: level.to_owned(), 87 | sort_index: sort_index.unwrap_or(100), 88 | user_id: user_id.to_owned(), 89 | parent_id: parent_id.to_owned(), 90 | status_id: Some("A".to_owned()), 91 | }; 92 | 93 | let category = updateCategory(repo_category, &input).await?; 94 | Ok(category) 95 | } 96 | 97 | #[derive(Debug, Clone)] 98 | pub struct CreateCategoryInput { 99 | pub name: String, 100 | pub description: Option, 101 | pub postable: bool, 102 | pub level: u32, 103 | pub parent_id: Option, 104 | pub sort_index: Option, 105 | pub user_id: Option, 106 | } 107 | -------------------------------------------------------------------------------- /libs/core/src/service/category/utils/delete.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_category::{RepoCategory, RepoCategoryTrait}, 3 | service::category::{errors::CategoryError, utils::find_by_id::execute as find_by_id}, 4 | }; 5 | use error_stack::{Result, ResultExt}; 6 | 7 | pub async fn execute(repo_category: &RepoCategory, id: &str) -> Result<(), CategoryError> { 8 | find_by_id(repo_category, id) 9 | .await? 10 | .ok_or(CategoryError::CategoryNotExistError)?; 11 | 12 | repo_category 13 | .delete(id) 14 | .await 15 | .change_context(CategoryError::InternalServerError)?; 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /libs/core/src/service/category/utils/find_by_id.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_category::{Category, RepoCategory, RepoCategoryTrait}, 3 | service::category::errors::CategoryError, 4 | }; 5 | use error_stack::Result; 6 | use tracing::error; 7 | 8 | pub async fn execute( 9 | repo_category: &RepoCategory, 10 | id: &str, 11 | ) -> Result, CategoryError> { 12 | let category = repo_category.find_by_id(id).await.map_err(|error| { 13 | error!("{:#?}", error); 14 | CategoryError::InternalServerError 15 | })?; 16 | Ok(category) 17 | } 18 | -------------------------------------------------------------------------------- /libs/core/src/service/category/utils/find_by_slug.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_category::{Category, RepoCategory, RepoCategoryTrait}, 3 | service::category::errors::CategoryError, 4 | }; 5 | use error_stack::Result; 6 | use tracing::error; 7 | 8 | pub async fn execute( 9 | repo_category: &RepoCategory, 10 | slug: &str, 11 | ) -> Result, CategoryError> { 12 | let category = repo_category.find_by_slug(slug).await.map_err(|error| { 13 | error!("{:#?}", error); 14 | CategoryError::InternalServerError 15 | })?; 16 | 17 | if let Some(category) = category { 18 | if category.status_id == "A" { 19 | return Ok(Some(category)); 20 | } 21 | } 22 | Ok(None) 23 | } 24 | -------------------------------------------------------------------------------- /libs/core/src/service/category/utils/get_categories.rs: -------------------------------------------------------------------------------- 1 | use error_stack::{Result, ResultExt}; 2 | use tokio::try_join; 3 | 4 | pub use crate::repository::repo_category::ListCategoryInput; 5 | 6 | use crate::{ 7 | repository::repo_category::{Category, RepoCategory, RepoCategoryTrait}, 8 | service::category::errors::CategoryError, 9 | }; 10 | 11 | pub async fn execute( 12 | repo_category: &RepoCategory, 13 | input: Option<&ListCategoryInput>, 14 | ) -> Result { 15 | let default_input = ListCategoryInput::default(); 16 | let input = input.unwrap_or(&default_input); 17 | let (data, count) = try_join!( 18 | repo_category.list(input), 19 | repo_category.count(&input.filter) 20 | ) 21 | .change_context(CategoryError::InternalServerError)?; 22 | Ok(GetCategoriesResponse { data, count }) 23 | } 24 | 25 | #[derive(Debug, Clone, Deserialize, Serialize)] 26 | pub struct GetCategoriesResponse { 27 | pub data: Vec, 28 | pub count: u64, 29 | } 30 | -------------------------------------------------------------------------------- /libs/core/src/service/category/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create; 2 | pub mod delete; 3 | pub mod find_by_id; 4 | pub mod find_by_slug; 5 | pub mod get_categories; 6 | pub mod update; 7 | -------------------------------------------------------------------------------- /libs/core/src/service/category/utils/update.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_category::{self, Category, RepoCategory, RepoCategoryTrait}, 3 | service::category::{errors::CategoryError, utils::find_by_id::execute as find_by_id}, 4 | }; 5 | use error_stack::{Result, ResultExt}; 6 | use slugify::slugify; 7 | 8 | pub async fn execute( 9 | repo_category: &RepoCategory, 10 | input: &UpdateCategoryInput, 11 | ) -> Result { 12 | let category = find_by_id(repo_category, &input.id) 13 | .await? 14 | .ok_or(CategoryError::CategoryNotExistError)?; 15 | 16 | let input = repo_category::UpdateCategoryInput { 17 | id: input.id.to_owned(), 18 | name: input.name.to_owned(), 19 | slug: slugify!(&input.name), 20 | parent_id: input.parent_id.to_owned(), 21 | description: input.description.to_owned(), 22 | postable: input.postable.to_owned(), 23 | sort_index: input.sort_index.to_owned(), 24 | level: input.level.to_owned(), 25 | user_id: input.user_id.to_owned(), 26 | status_id: input.status_id.to_owned().unwrap_or(category.status_id), 27 | }; 28 | 29 | let category = repo_category 30 | .update(&input) 31 | .await 32 | .change_context(CategoryError::InternalServerError)?; 33 | 34 | Ok(category) 35 | } 36 | 37 | #[derive(Debug, Clone)] 38 | pub struct UpdateCategoryInput { 39 | pub id: String, 40 | pub name: String, 41 | pub description: Option, 42 | pub postable: bool, 43 | pub level: u32, 44 | pub sort_index: i32, 45 | pub parent_id: Option, 46 | pub user_id: Option, 47 | pub status_id: Option, 48 | } 49 | -------------------------------------------------------------------------------- /libs/core/src/service/config/config_service.rs: -------------------------------------------------------------------------------- 1 | use error_stack::Result; 2 | use std::fmt::Debug; 3 | 4 | pub use crate::{ 5 | repository::repo_config::AppsVersion, service::config::utils::get_config::AppConfig, 6 | }; 7 | use crate::{ 8 | repository::repo_config::RepoConfig, 9 | service::config::{errors::ConfigError, utils}, 10 | }; 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct ConfigService { 14 | pub repo_config: RepoConfig, 15 | } 16 | 17 | #[async_trait] 18 | pub trait ConfigServiceTrait: Sync + Send + Debug { 19 | async fn get_config(&self) -> Result; 20 | } 21 | 22 | #[async_trait] 23 | impl ConfigServiceTrait for ConfigService { 24 | async fn get_config(&self) -> Result { 25 | utils::get_config::execute(&self.repo_config).await 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /libs/core/src/service/config/constants.rs: -------------------------------------------------------------------------------- 1 | use strum_macros::{Display, EnumString}; 2 | 3 | #[derive(Display, EnumString, Debug, PartialEq, Eq)] 4 | pub enum AppStatus { 5 | #[strum(serialize = "normal")] 6 | Normal, 7 | #[strum(serialize = "maintaining")] 8 | Maintaining, 9 | } 10 | -------------------------------------------------------------------------------- /libs/core/src/service/config/errors.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Display, Error}; 2 | use strum_macros::EnumProperty; 3 | 4 | #[derive(Debug, Display, EnumProperty, Error)] 5 | pub enum ConfigError { 6 | #[display(fmt = "{_0}")] 7 | #[strum(props(code = "generic"))] 8 | Generic(#[error(not(source))] String), 9 | #[display(fmt = "Internal Server Error")] 10 | #[strum(props(code = "internal_server_error"))] 11 | InternalServerError, 12 | } 13 | -------------------------------------------------------------------------------- /libs/core/src/service/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config_service; 2 | pub mod constants; 3 | pub mod errors; 4 | pub mod utils; 5 | -------------------------------------------------------------------------------- /libs/core/src/service/config/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_config; 2 | -------------------------------------------------------------------------------- /libs/core/src/service/factory.rs: -------------------------------------------------------------------------------- 1 | use aws_config::SdkConfig; 2 | use sea_orm::DatabaseConnection; 3 | use std::sync::Arc; 4 | 5 | use crate::{ 6 | repository::{ 7 | database::{ 8 | db_category::DbCategory, db_config::DbConfig, db_file::DbFile, db_post::DbPost, 9 | db_user::DbUser, 10 | }, 11 | repo_category::RepoCategory, 12 | repo_config::RepoConfig, 13 | repo_file::RepoFile, 14 | repo_post::RepoPost, 15 | repo_user::RepoUser, 16 | }, 17 | service::{ 18 | auth::{ 19 | auth_service::{AuthService, AuthServiceTrait}, 20 | provider::api_provider::ApiCognito, 21 | }, 22 | category::category_service::CategoryService, 23 | config::config_service::ConfigService, 24 | file::{file_service::FileService, provider::api_provider::ApiS3}, 25 | post::post_service::PostService, 26 | user::user_service::UserService, 27 | }, 28 | }; 29 | 30 | pub struct Factory { 31 | db_connection: Arc, 32 | sdk_config: Arc, 33 | } 34 | 35 | impl Factory { 36 | pub fn new(db_connection: &Arc, sdk_config: &Arc) -> Factory { 37 | Factory { 38 | sdk_config: sdk_config.clone(), 39 | db_connection: db_connection.clone(), 40 | } 41 | } 42 | 43 | pub fn new_auth_service(&self) -> AuthService { 44 | let api_provider = Arc::new(ApiCognito::new(&self.sdk_config)); 45 | AuthService { api_provider } 46 | } 47 | 48 | pub fn new_config_service(&self) -> ConfigService { 49 | let db_config = DbConfig::new(&self.db_connection); 50 | let repo_config = RepoConfig::new(db_config); 51 | ConfigService { repo_config } 52 | } 53 | 54 | pub fn new_file_service(&self) -> FileService { 55 | let db_file = DbFile::new(&self.db_connection); 56 | let repo_file = RepoFile::new(db_file); 57 | let api_provider = Arc::new(ApiS3::new(&self.sdk_config)); 58 | FileService { 59 | repo_file, 60 | api_provider, 61 | } 62 | } 63 | 64 | pub fn new_post_service(&self) -> PostService { 65 | let db_post = DbPost::new(&self.db_connection); 66 | let repo_post = RepoPost::new(db_post); 67 | PostService { repo_post } 68 | } 69 | 70 | pub fn new_category_service(&self) -> CategoryService { 71 | let category = DbCategory::new(&self.db_connection); 72 | let repo_category = RepoCategory::new(category); 73 | CategoryService { repo_category } 74 | } 75 | 76 | pub fn new_user_service(&self, auth_service: Arc) -> UserService { 77 | let db_user = DbUser::new(&self.db_connection); 78 | let repo_user = RepoUser::new(db_user); 79 | UserService { 80 | repo_user, 81 | auth_service, 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /libs/core/src/service/file/constants.rs: -------------------------------------------------------------------------------- 1 | use strum_macros::EnumString; 2 | 3 | #[derive(EnumString, Debug)] 4 | pub enum FileType { 5 | #[strum(serialize = "avatar_uri")] 6 | AvatarUri, 7 | #[strum(serialize = "attachment")] 8 | Attachment, 9 | } 10 | -------------------------------------------------------------------------------- /libs/core/src/service/file/errors.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Display, Error}; 2 | 3 | #[derive(Debug, Display, Error)] 4 | pub enum FileError { 5 | #[display(fmt = "File Error: Generic {_0}")] 6 | Generic(#[error(not(source))] String), 7 | #[display(fmt = "File Error: Unable to get the upload url")] 8 | UnableToGetTheUploadUrl, 9 | #[display(fmt = "File Error: Unable to retrieve the file")] 10 | UnableToRetrieveFile, 11 | } 12 | -------------------------------------------------------------------------------- /libs/core/src/service/file/file_service.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_file::RepoFile, 3 | service::file::{ 4 | constants::FileType, 5 | errors::FileError, 6 | provider::api_provider::ApiS3Trait, 7 | utils::{ 8 | get_file::get_file, 9 | get_upload_url::{get_upload_url, GetUploadUrlResponse}, 10 | }, 11 | }, 12 | }; 13 | use error_stack::Result; 14 | use std::{fmt::Debug, sync::Arc}; 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct FileService { 18 | pub repo_file: RepoFile, 19 | pub api_provider: Arc, 20 | } 21 | 22 | #[async_trait] 23 | pub trait FileServiceTrait: Sync + Send + Debug { 24 | // Get the private file by file id 25 | async fn get_file(&self, key: &str) -> Result; 26 | // Get the presigned upload url 27 | async fn get_upload_url(&self, file_type: &FileType) 28 | -> Result; 29 | } 30 | 31 | #[async_trait] 32 | impl FileServiceTrait for FileService { 33 | async fn get_file(&self, key: &str) -> Result { 34 | get_file(&self.repo_file, &*self.api_provider, key).await 35 | } 36 | async fn get_upload_url( 37 | &self, 38 | file_type: &FileType, 39 | ) -> Result { 40 | get_upload_url(&*self.api_provider, file_type).await 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /libs/core/src/service/file/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod errors; 3 | pub mod file_service; 4 | pub mod provider; 5 | pub mod utils; 6 | -------------------------------------------------------------------------------- /libs/core/src/service/file/provider/api_provider.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, sync::Arc}; 2 | 3 | use aws_config::SdkConfig; 4 | use discuz_utils::config::{get_config, S3}; 5 | use error_stack::Result; 6 | 7 | use aws_sdk_s3::Client; 8 | 9 | use crate::service::file::provider::{ 10 | errors::ProviderError, 11 | utils::{delete_file, get_file, get_upload_url}, 12 | }; 13 | 14 | pub use crate::service::file::provider::utils::{GetUploadUrlInput, GetUploadUrlResponse}; 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct ApiS3 { 18 | pub client: Client, 19 | pub bucket: String, 20 | } 21 | 22 | impl ApiS3 { 23 | pub fn new(sdk_config: &Arc) -> ApiS3 { 24 | let config = get_config().clone(); 25 | let client = Client::new(sdk_config); 26 | let S3 { bucket } = config.amazon.s3; 27 | ApiS3 { client, bucket } 28 | } 29 | } 30 | 31 | #[async_trait] 32 | pub trait ApiS3Trait: Sync + Send + Debug { 33 | async fn get_file(&self, key: &str, expires_in: Option) -> Result; 34 | async fn delete_file(&self, key: &str) -> Result<(), ProviderError>; 35 | async fn get_upload_url( 36 | &self, 37 | input: &GetUploadUrlInput, 38 | ) -> Result; 39 | } 40 | 41 | #[async_trait] 42 | impl ApiS3Trait for ApiS3 { 43 | async fn get_file(&self, key: &str, expires_in: Option) -> Result { 44 | get_file(&self.client, &self.bucket, key, expires_in).await 45 | } 46 | 47 | async fn delete_file(&self, key: &str) -> Result<(), ProviderError> { 48 | delete_file(&self.client, &self.bucket, key).await 49 | } 50 | 51 | async fn get_upload_url( 52 | &self, 53 | input: &GetUploadUrlInput, 54 | ) -> Result { 55 | get_upload_url(&self.client, &self.bucket, input).await 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /libs/core/src/service/file/provider/errors.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Display, Error}; 2 | 3 | #[derive(Debug, Display, Error)] 4 | pub enum ProviderError { 5 | #[display(fmt = "Provider Error: Generic {_0}")] 6 | Generic(#[error(not(source))] String), 7 | #[display(fmt = "Provider Error: Invalid Presigning Config")] 8 | InvalidPresigningConfig, 9 | #[display(fmt = "Provider Error: Invalid Presigned Url Request")] 10 | InvalidPresignedUrlRequest, 11 | } 12 | -------------------------------------------------------------------------------- /libs/core/src/service/file/provider/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api_provider; 2 | pub mod errors; 3 | mod utils; 4 | -------------------------------------------------------------------------------- /libs/core/src/service/file/provider/utils/delete_file.rs: -------------------------------------------------------------------------------- 1 | use crate::service::file::provider::errors::ProviderError; 2 | use aws_sdk_s3::Client; 3 | use error_stack::Result; 4 | 5 | pub async fn delete_file(client: &Client, bucket: &str, key: &str) -> Result<(), ProviderError> { 6 | client 7 | .delete_object() 8 | .bucket(bucket) 9 | .key(key) 10 | .send() 11 | .await 12 | .ok(); 13 | 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /libs/core/src/service/file/provider/utils/get_file.rs: -------------------------------------------------------------------------------- 1 | use crate::service::file::provider::errors::ProviderError; 2 | use aws_sdk_s3::{presigning::config::PresigningConfig, Client}; 3 | use error_stack::{IntoReport, Result, ResultExt}; 4 | use std::time::Duration; 5 | 6 | pub async fn get_file( 7 | client: &Client, 8 | bucket: &str, 9 | key: &str, 10 | expires_in: Option, 11 | ) -> Result { 12 | let presigning_config = 13 | PresigningConfig::expires_in(Duration::from_secs(expires_in.unwrap_or(60 * 60 * 24))) 14 | .into_report() 15 | .change_context(ProviderError::InvalidPresigningConfig)?; 16 | 17 | let url = client 18 | .get_object() 19 | .bucket(bucket) 20 | .key(key) 21 | .presigned(presigning_config) 22 | .await 23 | .into_report() 24 | .change_context(ProviderError::InvalidPresignedUrlRequest)? 25 | .uri() 26 | .to_string(); 27 | 28 | Ok(url) 29 | } 30 | -------------------------------------------------------------------------------- /libs/core/src/service/file/provider/utils/get_upload_url.rs: -------------------------------------------------------------------------------- 1 | use crate::{constants::BYTES_OF_1_MB, service::file::provider::errors::ProviderError}; 2 | use aws_sdk_s3::{model::ObjectCannedAcl, presigning::config::PresigningConfig, Client}; 3 | use error_stack::{IntoReport, Result, ResultExt}; 4 | use serde::Serialize; 5 | use std::time::Duration; 6 | use uuid::Uuid; 7 | 8 | pub async fn get_upload_url( 9 | client: &Client, 10 | bucket: &str, 11 | input: &GetUploadUrlInput, 12 | ) -> Result { 13 | let file_id = &Uuid::new_v4().to_string(); 14 | 15 | let presigning_config = PresigningConfig::expires_in(Duration::from_secs(input.expires_in)) 16 | .into_report() 17 | .change_context(ProviderError::InvalidPresigningConfig)?; 18 | 19 | let upload_uri = client 20 | .put_object() 21 | .bucket(bucket) 22 | .key(file_id) 23 | .acl(if input.is_public { 24 | ObjectCannedAcl::PublicRead 25 | } else { 26 | ObjectCannedAcl::Private 27 | }) 28 | .content_length(input.max_file_size) 29 | .presigned(presigning_config) 30 | .await 31 | .into_report() 32 | .change_context(ProviderError::InvalidPresignedUrlRequest)? 33 | .uri() 34 | .to_string(); 35 | 36 | let public_uri = if input.is_public { 37 | Some(format!("https://{bucket}.s3.amazonaws.com/{file_id}")) 38 | } else { 39 | None 40 | }; 41 | 42 | Ok(GetUploadUrlResponse { 43 | file_id: file_id.to_string(), 44 | upload_uri, 45 | public_uri, 46 | }) 47 | } 48 | 49 | #[derive(Debug, Clone)] 50 | pub struct GetUploadUrlInput { 51 | pub is_public: bool, 52 | pub expires_in: u64, 53 | pub max_file_size: i64, 54 | } 55 | 56 | impl Default for GetUploadUrlInput { 57 | fn default() -> Self { 58 | Self { 59 | is_public: false, 60 | expires_in: 60 * 15, 61 | max_file_size: 5 * BYTES_OF_1_MB, // 5mb 62 | } 63 | } 64 | } 65 | 66 | #[derive(Debug, Clone, Serialize)] 67 | pub struct GetUploadUrlResponse { 68 | pub file_id: String, 69 | pub upload_uri: String, 70 | pub public_uri: Option, 71 | } 72 | -------------------------------------------------------------------------------- /libs/core/src/service/file/provider/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod delete_file; 2 | mod get_file; 3 | mod get_upload_url; 4 | 5 | pub use delete_file::*; 6 | pub use get_file::*; 7 | pub use get_upload_url::*; 8 | -------------------------------------------------------------------------------- /libs/core/src/service/file/utils/get_file.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_file::{RepoFile, RepoFileTrait}, 3 | service::file::{errors::FileError, provider::api_provider::ApiS3Trait}, 4 | }; 5 | use error_stack::{FutureExt, Result, ResultExt}; 6 | 7 | pub async fn get_file( 8 | repo_file: &RepoFile, 9 | api_provider: &dyn ApiS3Trait, 10 | id: &str, 11 | ) -> Result { 12 | let file = repo_file 13 | .find_by_id(id) 14 | .change_context(FileError::Generic( 15 | "Unable to retrieve data from repository".to_owned(), 16 | )) 17 | .await? 18 | .ok_or(FileError::UnableToRetrieveFile)?; 19 | 20 | let url = api_provider 21 | .get_file(&file.id, None) 22 | .await 23 | .change_context(FileError::UnableToRetrieveFile)?; 24 | 25 | Ok(url) 26 | } 27 | -------------------------------------------------------------------------------- /libs/core/src/service/file/utils/get_upload_url.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | constants::BYTES_OF_1_MB, 3 | service::file::{ 4 | constants::FileType, 5 | errors::FileError, 6 | provider::api_provider::{ApiS3Trait, GetUploadUrlInput}, 7 | }, 8 | }; 9 | use error_stack::{Result, ResultExt}; 10 | 11 | pub use crate::service::file::provider::api_provider::GetUploadUrlResponse; 12 | 13 | pub async fn get_upload_url( 14 | api_provider: &dyn ApiS3Trait, 15 | file_type: &FileType, 16 | ) -> Result { 17 | let input = match file_type { 18 | FileType::AvatarUri => GetUploadUrlInput { 19 | is_public: true, 20 | max_file_size: BYTES_OF_1_MB * 5, 21 | ..GetUploadUrlInput::default() 22 | }, 23 | FileType::Attachment => GetUploadUrlInput { 24 | is_public: true, 25 | max_file_size: BYTES_OF_1_MB * 10, 26 | ..GetUploadUrlInput::default() 27 | }, 28 | }; 29 | let response = api_provider 30 | .get_upload_url(&input) 31 | .await 32 | .change_context(FileError::UnableToGetTheUploadUrl)?; 33 | Ok(response) 34 | } 35 | -------------------------------------------------------------------------------- /libs/core/src/service/file/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_file; 2 | pub mod get_upload_url; 3 | -------------------------------------------------------------------------------- /libs/core/src/service/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod category; 3 | pub mod config; 4 | pub mod factory; 5 | pub mod file; 6 | pub mod post; 7 | pub mod prelude; 8 | pub mod user; 9 | -------------------------------------------------------------------------------- /libs/core/src/service/post/errors.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Display, Error}; 2 | use strum_macros::EnumProperty; 3 | 4 | #[derive(Debug, Display, EnumProperty, Error)] 5 | pub enum PostError { 6 | #[display(fmt = "Post Error: Generic {_0}")] 7 | #[strum(props(code = "post_generic"))] 8 | Generic(#[error(not(source))] String), 9 | #[display(fmt = "Post Error: Internal Server Error")] 10 | #[strum(props(code = "post_internal_server_error"))] 11 | InternalServerError, 12 | #[display(fmt = "Post Error: Target post not exist")] 13 | #[strum(props(code = "post_not_exist"))] 14 | PostNotExistError, 15 | #[display(fmt = "Maximum reply reached")] 16 | #[strum(props(code = "post_maximum_reply"))] 17 | MaximumReplyError, 18 | } 19 | -------------------------------------------------------------------------------- /libs/core/src/service/post/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | pub mod post_service; 3 | pub mod utils; 4 | -------------------------------------------------------------------------------- /libs/core/src/service/post/post_service.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_post::RepoPost, 3 | service::post::{errors::PostError, utils}, 4 | }; 5 | pub use crate::{ 6 | repository::repo_post::{Post, PostReply}, 7 | service::post::utils::{ 8 | create::CreatePostInput, create_reply::CreateReplyInput, update::UpdatePostInput, 9 | }, 10 | }; 11 | 12 | use error_stack::Result; 13 | use std::fmt::Debug; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct PostService { 17 | pub repo_post: RepoPost, 18 | } 19 | 20 | #[async_trait] 21 | pub trait PostServiceTrait: Sync + Send + Debug { 22 | // Post 23 | async fn find_by_id(&self, id: i32) -> Result, PostError>; 24 | async fn create(&self, input: &CreatePostInput) -> Result; 25 | async fn update(&self, input: &UpdatePostInput) -> Result; 26 | async fn delete(&self, id: i32) -> Result<(), PostError>; 27 | // Reply 28 | async fn find_reply_by_id(&self, id: i32) -> Result, PostError>; 29 | async fn create_reply(&self, input: &CreateReplyInput) -> Result; 30 | async fn delete_reply(&self, id: i32) -> Result<(), PostError>; 31 | } 32 | 33 | #[async_trait] 34 | impl PostServiceTrait for PostService { 35 | async fn find_by_id(&self, id: i32) -> Result, PostError> { 36 | utils::find_by_id::execute(&self.repo_post, id).await 37 | } 38 | 39 | async fn create(&self, input: &CreatePostInput) -> Result { 40 | utils::create::execute(&self.repo_post, input).await 41 | } 42 | 43 | async fn update(&self, input: &UpdatePostInput) -> Result { 44 | utils::update::execute(&self.repo_post, input).await 45 | } 46 | 47 | async fn delete(&self, id: i32) -> Result<(), PostError> { 48 | utils::delete::execute(&self.repo_post, id).await 49 | } 50 | 51 | async fn find_reply_by_id(&self, id: i32) -> Result, PostError> { 52 | utils::find_reply_by_id::execute(&self.repo_post, id).await 53 | } 54 | 55 | async fn create_reply(&self, input: &CreateReplyInput) -> Result { 56 | utils::create_reply::execute(&self.repo_post, input).await 57 | } 58 | 59 | async fn delete_reply(&self, id: i32) -> Result<(), PostError> { 60 | utils::delete_reply::execute(&self.repo_post, id).await 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /libs/core/src/service/post/utils/create.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_post::{self, Post, RepoPost, RepoPostTrait}, 3 | service::post::{errors::PostError, utils::find_by_id::execute as find_by_id}, 4 | }; 5 | use chrono; 6 | use error_stack::{Result, ResultExt}; 7 | use slugify::slugify; 8 | 9 | pub async fn execute(repo_post: &RepoPost, input: &CreatePostInput) -> Result { 10 | let time = chrono::offset::Utc::now(); 11 | let slug = slugify!(&format!("{} {:?}", &input.title, time)); 12 | 13 | let CreatePostInput { 14 | title, 15 | category_id, 16 | content, 17 | user_id, 18 | } = input; 19 | 20 | let input = repo_post::CreatePostInput { 21 | title: title.to_owned(), 22 | slug, 23 | category_id: category_id.to_owned(), 24 | content: content.to_owned(), 25 | user_id: user_id.to_owned(), 26 | }; 27 | 28 | let post_id = repo_post 29 | .create(&input) 30 | .await 31 | .change_context(PostError::InternalServerError)?; 32 | let post = find_by_id(repo_post, post_id) 33 | .await? 34 | .ok_or(PostError::InternalServerError)?; 35 | 36 | Ok(post) 37 | } 38 | 39 | #[derive(Debug, Clone)] 40 | pub struct CreatePostInput { 41 | pub title: String, 42 | pub category_id: String, 43 | pub content: String, 44 | pub user_id: String, 45 | } 46 | -------------------------------------------------------------------------------- /libs/core/src/service/post/utils/create_reply.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_post::{ 3 | CreateReplyInput as RepoCreateReplyInput, PostReply, RepoPost, RepoPostTrait, 4 | }, 5 | service::post::{ 6 | errors::PostError, 7 | utils::{find_by_id, find_reply_by_id}, 8 | }, 9 | }; 10 | use error_stack::{Result, ResultExt}; 11 | 12 | pub async fn execute( 13 | repo_post: &RepoPost, 14 | input: &CreateReplyInput, 15 | ) -> Result { 16 | let post = find_by_id::execute(repo_post, input.post_id) 17 | .await? 18 | .ok_or(PostError::PostNotExistError)?; 19 | 20 | let next_no_of_reply = post.comment_count + 2; 21 | 22 | if next_no_of_reply > post.max_comment_count + 1 { 23 | return Err(PostError::MaximumReplyError.into()); 24 | } 25 | 26 | let input = RepoCreateReplyInput { 27 | post_id: input.post_id, 28 | quote_reply_id: input.quote_reply_id, 29 | no_of_reply: next_no_of_reply as u32, 30 | content: input.content.to_owned(), 31 | user_id: input.user_id.to_owned(), 32 | }; 33 | 34 | let reply_id = repo_post 35 | .create_reply(&input) 36 | .await 37 | .change_context(PostError::InternalServerError)?; 38 | 39 | let post_reply = find_reply_by_id::execute(repo_post, reply_id) 40 | .await? 41 | .ok_or(PostError::InternalServerError)?; 42 | 43 | Ok(post_reply) 44 | } 45 | 46 | pub struct CreateReplyInput { 47 | pub post_id: i32, 48 | pub quote_reply_id: Option, 49 | pub content: String, 50 | pub user_id: String, 51 | } 52 | -------------------------------------------------------------------------------- /libs/core/src/service/post/utils/delete.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_post::{RepoPost, RepoPostTrait}, 3 | service::post::{errors::PostError, utils::find_by_id::execute as find_by_id}, 4 | }; 5 | use error_stack::{Result, ResultExt}; 6 | 7 | pub async fn execute(repo_post: &RepoPost, id: i32) -> Result<(), PostError> { 8 | find_by_id(repo_post, id) 9 | .await? 10 | .ok_or(PostError::PostNotExistError)?; 11 | 12 | repo_post 13 | .delete(id) 14 | .await 15 | .change_context(PostError::InternalServerError)?; 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /libs/core/src/service/post/utils/delete_reply.rs: -------------------------------------------------------------------------------- 1 | pub use crate::repository::repo_post::CreateReplyInput; 2 | use crate::{ 3 | repository::repo_post::{RepoPost, RepoPostTrait}, 4 | service::post::errors::PostError, 5 | }; 6 | use error_stack::{FutureExt, Result}; 7 | 8 | pub async fn execute(repo_post: &RepoPost, id: i32) -> Result<(), PostError> { 9 | repo_post 10 | .delete_reply(id) 11 | .change_context(PostError::InternalServerError) 12 | .await?; 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /libs/core/src/service/post/utils/find_by_id.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_post::{Post, RepoPost, RepoPostTrait}, 3 | service::post::errors::PostError, 4 | }; 5 | use error_stack::Result; 6 | 7 | pub async fn execute(repo_post: &RepoPost, id: i32) -> Result, PostError> { 8 | let post = repo_post.find_by_id(id).await.map_err(|error| { 9 | println!("{error:#?}"); 10 | PostError::InternalServerError 11 | })?; 12 | Ok(post) 13 | } 14 | -------------------------------------------------------------------------------- /libs/core/src/service/post/utils/find_reply_by_id.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_post::{PostReply, RepoPost, RepoPostTrait}, 3 | service::post::errors::PostError, 4 | }; 5 | use error_stack::{FutureExt, Result}; 6 | 7 | pub async fn execute(repo_post: &RepoPost, id: i32) -> Result, PostError> { 8 | let post_reply = repo_post 9 | .find_reply_by_id(id) 10 | .change_context(PostError::InternalServerError) 11 | .await?; 12 | Ok(post_reply) 13 | } 14 | -------------------------------------------------------------------------------- /libs/core/src/service/post/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create; 2 | pub mod delete; 3 | pub mod find_by_id; 4 | pub mod update; 5 | 6 | pub mod create_reply; 7 | pub mod delete_reply; 8 | pub mod find_reply_by_id; 9 | -------------------------------------------------------------------------------- /libs/core/src/service/post/utils/update.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_post::{self, Post, RepoPost, RepoPostTrait}, 3 | service::post::{errors::PostError, utils::find_by_id}, 4 | }; 5 | 6 | use error_stack::{Result, ResultExt}; 7 | use slugify::slugify; 8 | 9 | pub async fn execute(repo_post: &RepoPost, input: &UpdatePostInput) -> Result { 10 | let post = find_by_id::execute(repo_post, input.id) 11 | .await? 12 | .ok_or(PostError::PostNotExistError)?; 13 | 14 | let input = repo_post::UpdatePostInput { 15 | id: input.id.to_owned(), 16 | title: input.title.to_owned(), 17 | slug: slugify!(&input.title), 18 | category_id: input.category_id.to_owned(), 19 | max_comment_count: input.max_comment_count, 20 | content: input.content.to_owned(), 21 | status_id: input.status_id.to_owned().unwrap_or(post.status_id), 22 | }; 23 | 24 | repo_post 25 | .update(&input) 26 | .await 27 | .change_context(PostError::InternalServerError)?; 28 | 29 | let post = find_by_id::execute(repo_post, input.id) 30 | .await? 31 | .ok_or(PostError::InternalServerError)?; 32 | Ok(post) 33 | } 34 | 35 | #[derive(Debug, Clone)] 36 | pub struct UpdatePostInput { 37 | pub id: i32, 38 | pub title: String, 39 | pub category_id: String, 40 | pub max_comment_count: Option, 41 | pub content: String, 42 | pub status_id: Option, 43 | } 44 | -------------------------------------------------------------------------------- /libs/core/src/service/prelude.rs: -------------------------------------------------------------------------------- 1 | // Factory 2 | pub use crate::service::factory::Factory; 3 | 4 | // Services 5 | pub use crate::service::{ 6 | auth::auth_service::AuthService, category::category_service::CategoryService, 7 | config::config_service::ConfigService, file::file_service::FileService, 8 | post::post_service::PostService, user::user_service::UserService, 9 | }; 10 | 11 | // Traits 12 | pub use crate::service::{ 13 | auth::auth_service::AuthServiceTrait, category::category_service::CategoryServiceTrait, 14 | config::config_service::ConfigServiceTrait, file::file_service::FileServiceTrait, 15 | post::post_service::PostServiceTrait, user::user_service::UserServiceTrait, 16 | }; 17 | 18 | // Service function inputs 19 | pub use crate::service::{ 20 | category::category_service::{ 21 | CreateCategoryInput, GetCategoriesResponse, ListCategoryInput, UpdateCategoryInput, 22 | }, 23 | config::config_service::AppConfig, 24 | post::post_service::{CreatePostInput, CreateReplyInput, UpdatePostInput}, 25 | user::user_service::{BanUserInput, UpdateBanHistoryInput, UpdateUserInput}, 26 | }; 27 | -------------------------------------------------------------------------------- /libs/core/src/service/user/constants.rs: -------------------------------------------------------------------------------- 1 | use strum_macros::{Display, EnumMessage, EnumString}; 2 | 3 | #[derive(Display, Debug, EnumString, EnumMessage)] 4 | pub enum UserStatus { 5 | #[strum(serialize = "normal")] 6 | Normal, 7 | #[strum( 8 | serialize = "banned", 9 | detailed_message = "Banned users are unable to create or reply to posts" 10 | )] 11 | Banned, 12 | #[strum( 13 | serialize = "deactivated", 14 | detailed_message = "Deactivated users are no longer able to log in to the forum. Their previous posts and comments will be archived for reference, but will not be visible on the active site. " 15 | )] 16 | Deactivated, 17 | } 18 | -------------------------------------------------------------------------------- /libs/core/src/service/user/errors.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Display, Error}; 2 | use strum_macros::EnumProperty; 3 | 4 | #[derive(Debug, Display, EnumProperty, Error)] 5 | pub enum UserError { 6 | #[display(fmt = "Generic")] 7 | #[strum(props(code = "user_generic"))] 8 | Generic, 9 | #[display(fmt = "Invalid Code")] 10 | #[strum(props(code = "user_invalid_code"))] 11 | InvalidCode, 12 | #[display(fmt = "Invalid Credentials")] 13 | InvalidCredentials, 14 | #[display(fmt = "Target history not exist")] 15 | #[strum(props(code = "user_ban_history_not_exist"))] 16 | UserBanHistoryNotExistError, 17 | #[display(fmt = "Internal Server Error")] 18 | #[strum(props(code = "user_internal_server_error"))] 19 | InternalServerError, 20 | } 21 | -------------------------------------------------------------------------------- /libs/core/src/service/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod errors; 3 | pub mod user_service; 4 | mod utils; 5 | -------------------------------------------------------------------------------- /libs/core/src/service/user/utils/ban_user_account.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_user::{BanUserInput, RepoUser, RepoUserTrait, UserBanHistory}, 3 | service::user::{constants::UserStatus, errors::UserError, utils}, 4 | }; 5 | use error_stack::{Result, ResultExt}; 6 | 7 | pub async fn execute( 8 | repo_user: &RepoUser, 9 | input: &BanUserInput, 10 | ) -> Result { 11 | let user_ban_history_id = repo_user 12 | .create_ban_history(input) 13 | .await 14 | .change_context(UserError::InternalServerError)?; 15 | 16 | repo_user 17 | .update_user_status(&input.ban_user_id.to_owned(), &UserStatus::Banned) 18 | .await 19 | .change_context(UserError::InternalServerError)?; 20 | 21 | let user_ban_history = 22 | utils::find_user_ban_history_by_id::execute(repo_user, user_ban_history_id) 23 | .await? 24 | .ok_or(UserError::InternalServerError)?; 25 | 26 | Ok(user_ban_history) 27 | } 28 | -------------------------------------------------------------------------------- /libs/core/src/service/user/utils/delete_user_ban_history.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_user::{RepoUser, RepoUserTrait}, 3 | service::user::errors::UserError, 4 | }; 5 | use error_stack::{Result, ResultExt}; 6 | 7 | pub async fn execute(repo_user: &RepoUser, user_id: &str) -> Result<(), UserError> { 8 | repo_user 9 | .update_user_ban_history_status_to_resolved(user_id) 10 | .await 11 | .change_context(UserError::InternalServerError)?; 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /libs/core/src/service/user/utils/find_user_ban_history_by_id.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repository::repo_user::{RepoUser, RepoUserTrait, UserBanHistory}, 3 | service::user::errors::UserError, 4 | }; 5 | use error_stack::{FutureExt, Result}; 6 | 7 | pub async fn execute(repo_user: &RepoUser, id: i32) -> Result, UserError> { 8 | let history = repo_user 9 | .find_ban_history_by_id(id) 10 | .change_context(UserError::InternalServerError) 11 | .await?; 12 | Ok(history) 13 | } 14 | -------------------------------------------------------------------------------- /libs/core/src/service/user/utils/get_profile.rs: -------------------------------------------------------------------------------- 1 | use error_stack::{Report, Result, ResultExt}; 2 | 3 | use crate::{ 4 | constants::{FAKE_ACCESS_TOKEN, FAKE_SUB}, 5 | repository::repo_user::{CreateUserInput, RepoUser, RepoUserTrait}, 6 | service::{ 7 | auth::auth_service::{AuthServiceTrait, AuthUser}, 8 | user::{errors::UserError, user_service::User}, 9 | }, 10 | }; 11 | 12 | use discuz_utils::config::get_config; 13 | 14 | pub async fn execute( 15 | repo_user: &RepoUser, 16 | auth_service: &dyn AuthServiceTrait, 17 | access_token: &str, 18 | ) -> Result { 19 | let sub = if !get_config().is_production() && access_token == FAKE_ACCESS_TOKEN { 20 | FAKE_SUB.to_owned() 21 | } else { 22 | auth_service 23 | .validate_access_token(access_token) 24 | .await 25 | .change_context(UserError::InvalidCredentials) 26 | .attach_printable("Invalid access token")? 27 | }; 28 | 29 | if let Some(user) = repo_user 30 | .find_by_sub(&sub) 31 | .await 32 | .change_context(UserError::Generic)? 33 | { 34 | return Ok(user); 35 | } 36 | 37 | let auth_user = auth_service 38 | .get_auth_user_by_access_token(access_token) 39 | .await 40 | .change_context(UserError::Generic)?; 41 | 42 | create_profile_by_cognito_user(repo_user, &auth_user).await 43 | } 44 | 45 | async fn create_profile_by_cognito_user( 46 | repo_user: &RepoUser, 47 | auth_user: &AuthUser, 48 | ) -> Result { 49 | let data = auth_user.clone(); 50 | 51 | let input = CreateUserInput { 52 | name: data.name, 53 | email: data.email, 54 | sub: data.sub, 55 | avatar_url: data.picture, 56 | }; 57 | 58 | let user_id = repo_user 59 | .create(&input) 60 | .await 61 | .change_context(UserError::Generic)?; 62 | 63 | if let Some(user) = repo_user 64 | .find_by_id(&user_id) 65 | .await 66 | .change_context(UserError::Generic)? 67 | { 68 | return Ok(user); 69 | } 70 | 71 | Err(Report::new(UserError::Generic)) 72 | } 73 | -------------------------------------------------------------------------------- /libs/core/src/service/user/utils/get_users.rs: -------------------------------------------------------------------------------- 1 | use error_stack::{Result, ResultExt}; 2 | use tokio::try_join; 3 | 4 | pub use crate::repository::repo_user::InputUserList; 5 | 6 | use crate::{ 7 | repository::repo_user::{RepoUser, RepoUserTrait}, 8 | service::user::{errors::UserError, user_service::User}, 9 | }; 10 | 11 | pub async fn execute( 12 | repo_user: &RepoUser, 13 | input: Option<&InputUserList>, 14 | ) -> Result { 15 | let default_input = InputUserList::default(); 16 | let input = input.unwrap_or(&default_input); 17 | let (data, count) = try_join!(repo_user.list(input), repo_user.count(&input.filter)) 18 | .change_context(UserError::InternalServerError)?; 19 | Ok(GetUsersResponse { data, count }) 20 | } 21 | 22 | #[derive(Debug, Clone)] 23 | pub struct GetUsersResponse { 24 | pub data: Vec, 25 | pub count: u64, 26 | } 27 | -------------------------------------------------------------------------------- /libs/core/src/service/user/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_profile; 2 | pub mod get_users; 3 | 4 | pub mod ban_user_account; 5 | pub mod delete_user_ban_history; 6 | pub mod find_user_ban_history_by_id; 7 | pub mod update_user_ban_history; 8 | -------------------------------------------------------------------------------- /libs/core/src/service/user/utils/update_user_ban_history.rs: -------------------------------------------------------------------------------- 1 | use error_stack::{Result, ResultExt}; 2 | 3 | use crate::{ 4 | repository::{ 5 | database::db_user::UpdateBanHistoryInput, 6 | repo_user::{RepoUser, RepoUserTrait, UserBanHistory}, 7 | }, 8 | service::user::{constants::UserStatus, errors::UserError, utils}, 9 | }; 10 | 11 | pub async fn execute( 12 | repo_user: &RepoUser, 13 | input: &UpdateBanHistoryInput, 14 | ) -> Result { 15 | let history = utils::find_user_ban_history_by_id::execute(repo_user, input.id) 16 | .await? 17 | .ok_or(UserError::UserBanHistoryNotExistError)?; 18 | 19 | let release_time = input.ban_time.map(|time| { 20 | let now = chrono::offset::Utc::now(); 21 | now + chrono::Duration::milliseconds(time as i64) 22 | }); 23 | 24 | let repo_input = UpdateBanHistoryInput { 25 | id: input.id.to_owned(), 26 | ban_user_id: input.ban_user_id.to_owned(), 27 | ban_reason: input.ban_reason.to_owned(), 28 | ban_time: input.ban_time.or(history.ban_time), 29 | release_time: release_time.or(history.release_time), 30 | user_id: input.user_id.to_owned(), 31 | }; 32 | 33 | repo_user 34 | .update_ban_history(&repo_input) 35 | .await 36 | .change_context(UserError::InternalServerError)?; 37 | 38 | let history = utils::find_user_ban_history_by_id::execute(repo_user, input.id) 39 | .await? 40 | .ok_or(UserError::InternalServerError)?; 41 | 42 | if let Some(release_time) = history.release_time { 43 | if chrono::offset::Utc::now() > release_time { 44 | repo_user 45 | .update_user_status(&history.ban_user_id, &UserStatus::Normal) 46 | .await 47 | .change_context(UserError::InternalServerError)?; 48 | } 49 | } 50 | 51 | Ok(history) 52 | } 53 | -------------------------------------------------------------------------------- /libs/core/src/utils/db_tools.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::{DatabaseBackend, Statement}; 2 | use sea_orm_migration::SchemaManager; 3 | 4 | pub fn on_update_current_timestamp(manager: &SchemaManager) -> String { 5 | match manager.get_database_backend() { 6 | DatabaseBackend::MySql => { 7 | "DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP".to_owned() 8 | } 9 | DatabaseBackend::Sqlite => "DEFAULT CURRENT_TIMESTAMP".to_owned(), 10 | _ => { 11 | panic!("on_update_current_timestamp function is work on MySql and Sqlite backend only") 12 | } 13 | } 14 | } 15 | 16 | pub fn add_column_after(manager: &SchemaManager, column: &str) -> String { 17 | match manager.get_database_backend() { 18 | DatabaseBackend::MySql => format!("AFTER {column}"), 19 | _ => "".to_owned(), 20 | } 21 | } 22 | 23 | pub fn create_unique_key( 24 | table_name: &str, 25 | index_name: &str, 26 | column_list: Vec, 27 | backend: DatabaseBackend, 28 | ) -> Statement { 29 | let string = match backend { 30 | DatabaseBackend::Sqlite | DatabaseBackend::MySql => format!( 31 | "CREATE UNIQUE INDEX {index_name} ON {table_name} ({})", 32 | column_list.join(",") 33 | ), 34 | _ => panic!("create_unique_key function is work on Sqlite and MySql backend only"), 35 | }; 36 | Statement::from_string(backend, string) 37 | } 38 | -------------------------------------------------------------------------------- /libs/core/src/utils/mock_data/apps_version.rs: -------------------------------------------------------------------------------- 1 | use crate::repository::database::entities::apps_version; 2 | use sea_orm::{DatabaseConnection, EntityTrait, Set}; 3 | use uuid::Uuid; 4 | 5 | pub const PACKAGE_ID: &str = "DEFAULT_PACKAGE_ID"; 6 | 7 | pub async fn create_mock_apps_version(db_connection: &DatabaseConnection) -> String { 8 | let id = Uuid::new_v4().to_string(); 9 | 10 | let apps_version = apps_version::ActiveModel { 11 | id: Set(id.to_owned()), 12 | platform: Set("ios".to_owned()), 13 | package_id: Set(PACKAGE_ID.to_owned()), 14 | current_version: Set("1.0.0".to_owned()), 15 | minimal_version: Set("1.0.0".to_owned()), 16 | ..Default::default() 17 | }; 18 | apps_version::Entity::insert(apps_version) 19 | .exec(db_connection) 20 | .await 21 | .unwrap(); 22 | 23 | id 24 | } 25 | -------------------------------------------------------------------------------- /libs/core/src/utils/mock_data/mock_cognito_provider.rs: -------------------------------------------------------------------------------- 1 | use crate::service::auth::provider::{ 2 | api_provider::{ApiCognitoTrait, GetTokensOutput}, 3 | errors::ProviderError, 4 | }; 5 | use ::fake::Fake; 6 | use aws_sdk_cognitoidentityprovider::{model::AttributeType, output::GetUserOutput}; 7 | use error_stack::Result; 8 | use fake::faker; 9 | use std::collections::HashMap; 10 | use tokio::sync::Mutex; 11 | 12 | lazy_static! { 13 | static ref USER_CACHE: Mutex> = Mutex::new(HashMap::new()); 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct MockApiCognito; 18 | 19 | #[async_trait] 20 | impl ApiCognitoTrait for MockApiCognito { 21 | async fn get_user_by_access_token( 22 | &self, 23 | access_token: &str, 24 | ) -> Result { 25 | if let Some(user) = USER_CACHE.lock().await.get(access_token) { 26 | return Ok(user.to_owned()); 27 | } 28 | 29 | let name: String = faker::name::en::Name().fake(); 30 | let email: String = faker::internet::en::SafeEmail().fake(); 31 | let picture = format!("https://i.pravatar.cc/250?u={access_token}"); 32 | 33 | let sub = AttributeType::builder() 34 | .set_name(Some("sub".to_owned())) 35 | .set_value(Some(access_token.to_owned())) 36 | .build(); 37 | 38 | let email_verified = AttributeType::builder() 39 | .set_name(Some("email_verified".to_owned())) 40 | .set_value(Some("true".to_owned())) 41 | .build(); 42 | 43 | let name = AttributeType::builder() 44 | .set_name(Some("name".to_owned())) 45 | .set_value(Some(name)) 46 | .build(); 47 | 48 | let phone_number_verified = AttributeType::builder() 49 | .set_name(Some("phone_number_verified".to_owned())) 50 | .set_value(Some("true".to_owned())) 51 | .build(); 52 | 53 | let email = AttributeType::builder() 54 | .set_name(Some("email".to_owned())) 55 | .set_value(Some(email)) 56 | .build(); 57 | 58 | let picture = AttributeType::builder() 59 | .set_name(Some("picture".to_owned())) 60 | .set_value(Some(picture)) 61 | .build(); 62 | 63 | let output = GetUserOutput::builder() 64 | .set_username(Some(access_token.to_owned())) 65 | .set_user_attributes(Some(vec![ 66 | sub, 67 | email_verified, 68 | name, 69 | phone_number_verified, 70 | email, 71 | picture, 72 | ])) 73 | .build(); 74 | 75 | USER_CACHE 76 | .lock() 77 | .await 78 | .insert(access_token.to_owned(), output.to_owned()); 79 | 80 | Ok(output.to_owned()) 81 | } 82 | 83 | async fn validate_access_token(&self, access_token: &str) -> Result { 84 | Ok(access_token.to_owned()) 85 | } 86 | 87 | async fn get_tokens(&self, _code: &str) -> Result { 88 | todo!() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /libs/core/src/utils/mock_data/mock_s3_provider.rs: -------------------------------------------------------------------------------- 1 | use crate::service::file::{ 2 | provider::{ 3 | api_provider::{ApiS3Trait, GetUploadUrlInput}, 4 | errors::ProviderError, 5 | }, 6 | utils::get_upload_url::GetUploadUrlResponse, 7 | }; 8 | use error_stack::Result; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct MockApiS3; 12 | 13 | #[async_trait] 14 | impl ApiS3Trait for MockApiS3 { 15 | async fn get_file( 16 | &self, 17 | _key: &str, 18 | _expires_in: Option, 19 | ) -> Result { 20 | Ok("FILE".to_owned()) 21 | } 22 | 23 | async fn delete_file(&self, _key: &str) -> Result<(), ProviderError> { 24 | Ok(()) 25 | } 26 | 27 | async fn get_upload_url( 28 | &self, 29 | _input: &GetUploadUrlInput, 30 | ) -> Result { 31 | Ok(GetUploadUrlResponse { 32 | file_id: uuid::Uuid::new_v4().to_string(), 33 | upload_uri: "UPLOAD_URI".to_owned(), 34 | public_uri: Some("PUBLIC_URI".to_owned()), 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libs/core/src/utils/mock_data/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod apps_version; 2 | pub mod mock_cognito_provider; 3 | pub mod mock_s3_provider; 4 | pub mod post; 5 | -------------------------------------------------------------------------------- /libs/core/src/utils/mock_data/post.rs: -------------------------------------------------------------------------------- 1 | pub const POST_TITLE: &str = "Lorem Ipsum"; 2 | pub const POST_SECOND_TITLE: &str = "Lorem Ipsum 2"; 3 | pub const POST_CONTENT: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sem quam. Suspendisse aliquet odio non elementum condimentum. Phasellus vel massa sodales, porttitor nibh nec, dapibus sem. Aenean vel est eu enim tempus pellentesque ac nec risus. Donec fringilla quis massa eu varius. Praesent nisi quam, imperdiet id malesuada sed, aliquet id ante. Etiam egestas ipsum lectus, id venenatis nulla ultricies ut. Quisque maximus tincidunt egestas. Maecenas vestibulum, risus eu laoreet commodo, est nulla pellentesque est, sollicitudin accumsan mauris ligula et velit. Mauris luctus auctor dui. Sed ultricies dictum justo, vel iaculis est gravida venenatis. Nunc auctor non sapien nec rutrum. Fusce condimentum et augue vitae convallis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin facilisis molestie interdum. Quisque non condimentum purus, ac rutrum augue."; 4 | pub const COMMENT_CONTENT: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."; 5 | -------------------------------------------------------------------------------- /libs/core/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db_tools; 2 | pub mod mock_data; 3 | -------------------------------------------------------------------------------- /libs/core/tests/unit/auth.rs: -------------------------------------------------------------------------------- 1 | use discuz_core::{ 2 | constants::{FAKE_ACCESS_TOKEN, FAKE_SUB}, 3 | migration::{Migrator, MigratorTrait}, 4 | repository::{database::db_user::DbUser, repo_user::RepoUser}, 5 | service::{ 6 | auth::{ 7 | auth_service::{AuthService, AuthServiceTrait}, 8 | constants::UserRole, 9 | utils::mock_data::should_return_mock_user_by_access_token, 10 | }, 11 | factory::Factory, 12 | user::user_service::{UserService, UserServiceTrait}, 13 | }, 14 | }; 15 | use discuz_utils::{amazon::get_aws_sdk_config, get_db_connection}; 16 | use std::sync::Arc; 17 | 18 | #[tokio::test] 19 | async fn get_fake_user() { 20 | assert!(should_return_mock_user_by_access_token(FAKE_ACCESS_TOKEN)); 21 | let SetupResponse { auth_service, .. } = setup().await; 22 | let sub_res = auth_service.validate_access_token(FAKE_ACCESS_TOKEN).await; 23 | assert!(sub_res.is_ok()); 24 | assert_eq!(sub_res.unwrap(), FAKE_SUB); 25 | let auth_user_res = auth_service 26 | .get_auth_user_by_access_token(FAKE_ACCESS_TOKEN) 27 | .await; 28 | assert!(auth_user_res.is_ok()); 29 | } 30 | 31 | #[tokio::test] 32 | async fn should_return_error_by_wrong_access_token() { 33 | let SetupResponse { auth_service, .. } = setup().await; 34 | let sub_res = auth_service 35 | .validate_access_token("FALSE ACCESS TOKEN") 36 | .await; 37 | assert!(sub_res.is_err()); 38 | } 39 | 40 | #[tokio::test] 41 | async fn should_reject_if_user_is_not_permitted() { 42 | let SetupResponse { 43 | auth_service, 44 | user_service, 45 | .. 46 | } = setup().await; 47 | 48 | let user = user_service.get_profile(FAKE_ACCESS_TOKEN).await.unwrap(); 49 | assert_eq!(user.role, UserRole::User.to_string()); 50 | let is_permitted = auth_service.validate_user(&user, Some(&[UserRole::Admin])); 51 | assert!(is_permitted.is_err()); 52 | } 53 | 54 | #[tokio::test] 55 | async fn should_accept_if_user_is_permitted() { 56 | let SetupResponse { 57 | auth_service, 58 | user_service, 59 | .. 60 | } = setup().await; 61 | 62 | let user = user_service.get_profile(FAKE_ACCESS_TOKEN).await.unwrap(); 63 | assert_eq!(user.role, UserRole::User.to_string()); 64 | let is_permitted = auth_service.validate_user(&user, Some(&[UserRole::Admin, UserRole::User])); 65 | 66 | assert!(is_permitted.is_ok()); 67 | } 68 | 69 | async fn setup() -> SetupResponse { 70 | let db_connection = Arc::new(get_db_connection().await.unwrap()); 71 | let sdk_config = Arc::new(get_aws_sdk_config().await); 72 | Migrator::refresh(&db_connection).await.unwrap(); 73 | let db_user = DbUser::new(&db_connection); 74 | let repo_user = RepoUser::new(db_user); 75 | let factory = Factory::new(&db_connection, &sdk_config); 76 | let auth_service = Arc::new(factory.new_auth_service()); 77 | let user_service = Arc::new(UserService { 78 | repo_user, 79 | auth_service: auth_service.clone(), 80 | }); 81 | SetupResponse { 82 | auth_service, 83 | user_service, 84 | } 85 | } 86 | 87 | pub struct SetupResponse { 88 | auth_service: Arc, 89 | user_service: Arc, 90 | } 91 | -------------------------------------------------------------------------------- /libs/core/tests/unit/config.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use discuz_core::{ 4 | migration::{Migrator, MigratorTrait}, 5 | service::prelude::*, 6 | utils::mock_data::apps_version::create_mock_apps_version, 7 | }; 8 | use discuz_utils::{amazon::get_aws_sdk_config, get_db_connection}; 9 | 10 | #[tokio::test] 11 | async fn get_app_config() { 12 | let SetupResponse { config_service } = setup().await; 13 | let result = config_service.get_config().await; 14 | assert!(result.is_ok()); 15 | } 16 | 17 | async fn setup() -> SetupResponse { 18 | let db_connection = Arc::new(get_db_connection().await.unwrap()); 19 | let sdk_config = Arc::new(get_aws_sdk_config().await); 20 | let factory = Factory::new(&db_connection, &sdk_config); 21 | let config_service = Arc::new(factory.new_config_service()); 22 | Migrator::refresh(&db_connection).await.unwrap(); 23 | create_mock_apps_version(&db_connection).await; 24 | SetupResponse { config_service } 25 | } 26 | 27 | pub struct SetupResponse { 28 | config_service: Arc, 29 | } 30 | -------------------------------------------------------------------------------- /libs/core/tests/unit/file.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use discuz_core::{ 4 | migration::{Migrator, MigratorTrait}, 5 | repository::repo_file::{DbFile, RepoFile}, 6 | service::{file::constants::FileType, prelude::*}, 7 | utils::mock_data::mock_s3_provider::MockApiS3, 8 | }; 9 | use discuz_utils::get_db_connection; 10 | 11 | #[tokio::test] 12 | async fn delete_file() { 13 | let SetupResponse { file_service } = setup().await; 14 | let file = file_service 15 | .get_upload_url(&FileType::Attachment) 16 | .await 17 | .unwrap(); 18 | assert_eq!(file.upload_uri, "UPLOAD_URI"); 19 | assert_eq!(file.public_uri, Some("PUBLIC_URI".to_owned())); 20 | } 21 | 22 | async fn setup() -> SetupResponse { 23 | let db_connection = Arc::new(get_db_connection().await.unwrap()); 24 | let api_provider = Arc::new(MockApiS3); 25 | let db_file = DbFile::new(&db_connection.clone()); 26 | let repo_file = RepoFile::new(db_file); 27 | let file_service = FileService { 28 | api_provider, 29 | repo_file, 30 | }; 31 | Migrator::refresh(&db_connection).await.unwrap(); 32 | SetupResponse { 33 | file_service: Arc::new(file_service), 34 | } 35 | } 36 | 37 | pub struct SetupResponse { 38 | file_service: Arc, 39 | } 40 | -------------------------------------------------------------------------------- /libs/core/tests/unit/migration.rs: -------------------------------------------------------------------------------- 1 | use discuz_core::migration::{Migrator, MigratorTrait}; 2 | use discuz_utils::get_db_connection; 3 | use std::sync::Arc; 4 | 5 | #[tokio::test] 6 | async fn db_migration_test() { 7 | let db_connection = Arc::new(get_db_connection().await.unwrap()); 8 | Migrator::refresh(&db_connection).await.unwrap(); 9 | } 10 | -------------------------------------------------------------------------------- /libs/core/tests/unit/mock_cognito_provider.rs: -------------------------------------------------------------------------------- 1 | use discuz_core::{ 2 | service::auth::provider::api_provider::ApiCognitoTrait, 3 | utils::mock_data::mock_cognito_provider::MockApiCognito, 4 | }; 5 | use uuid::Uuid; 6 | 7 | #[tokio::test] 8 | async fn should_return_same_user() { 9 | let user_token = Uuid::new_v4().to_string(); 10 | let api_provider = MockApiCognito; 11 | 12 | let response_a = api_provider 13 | .get_user_by_access_token(&user_token) 14 | .await 15 | .unwrap(); 16 | 17 | let response_b = api_provider 18 | .get_user_by_access_token(&user_token) 19 | .await 20 | .unwrap(); 21 | 22 | let response_a_name = response_a 23 | .user_attributes() 24 | .unwrap() 25 | .to_owned() 26 | .iter() 27 | .find(|attribute| attribute.name().to_owned().unwrap() == "name") 28 | .unwrap() 29 | .value() 30 | .unwrap() 31 | .to_owned(); 32 | 33 | let response_b_name = response_b 34 | .user_attributes() 35 | .unwrap() 36 | .to_owned() 37 | .iter() 38 | .find(|attribute| attribute.name().to_owned().unwrap() == "name") 39 | .unwrap() 40 | .value() 41 | .unwrap() 42 | .to_owned(); 43 | 44 | assert_eq!(response_a_name, response_b_name); 45 | } 46 | -------------------------------------------------------------------------------- /libs/core/tests/unit/mod.rs: -------------------------------------------------------------------------------- 1 | mod auth; 2 | mod category; 3 | mod config; 4 | mod file; 5 | mod migration; 6 | mod mock_cognito_provider; 7 | mod post; 8 | mod post_reply; 9 | mod user; 10 | mod user_ban_history; 11 | -------------------------------------------------------------------------------- /libs/core/tests/unit/user.rs: -------------------------------------------------------------------------------- 1 | use discuz_core::{ 2 | constants::FAKE_ACCESS_TOKEN, 3 | migration::{Migrator, MigratorTrait}, 4 | repository::{database::db_user::DbUser, repo_user::RepoUser}, 5 | service::{ 6 | auth::auth_service::{AuthService, AuthServiceTrait}, 7 | factory::Factory, 8 | user::user_service::{GetUsersResponse, UpdateUserInput, UserService, UserServiceTrait}, 9 | }, 10 | }; 11 | use discuz_utils::{amazon::get_aws_sdk_config, get_db_connection}; 12 | use dotenv::dotenv; 13 | use std::sync::Arc; 14 | 15 | #[tokio::test] 16 | async fn user_get_profile() { 17 | let SetupResponse { 18 | auth_service, 19 | user_service, 20 | .. 21 | } = setup().await; 22 | let auth_user = auth_service 23 | .get_auth_user_by_access_token(FAKE_ACCESS_TOKEN) 24 | .await 25 | .unwrap(); 26 | let user = user_service.get_profile(FAKE_ACCESS_TOKEN).await.unwrap(); 27 | 28 | assert_eq!(auth_user.sub, user.sub); 29 | assert_eq!(auth_user.name, user.name); 30 | assert_eq!(auth_user.email, user.email); 31 | assert!(user.avatar_url.is_none()); 32 | } 33 | 34 | #[tokio::test] 35 | async fn user_get_users_basic() { 36 | let SetupResponse { user_service, .. } = setup().await; 37 | user_service.get_profile(FAKE_ACCESS_TOKEN).await.unwrap(); 38 | let GetUsersResponse { data, count } = user_service.get_users(None).await.unwrap(); 39 | assert_eq!(data.len(), 1); 40 | assert_eq!(count, 1); 41 | } 42 | 43 | #[tokio::test] 44 | async fn user_update_profile() { 45 | let SetupResponse { user_service, .. } = setup().await; 46 | let user = user_service.get_profile(FAKE_ACCESS_TOKEN).await.unwrap(); 47 | 48 | let update = UpdateUserInput { 49 | id: user.id.to_owned(), 50 | name: "ANOTHER_NAME".to_owned(), 51 | avatar_url: None, 52 | }; 53 | user_service.update(&update).await.unwrap(); 54 | let updated_user = user_service.find_by_id(&user.id).await.unwrap().unwrap(); 55 | 56 | assert_eq!(updated_user.name, "ANOTHER_NAME".to_owned()); 57 | assert!(updated_user.avatar_url.is_none()); 58 | } 59 | 60 | async fn setup() -> SetupResponse { 61 | dotenv().ok(); 62 | let db_connection = Arc::new(get_db_connection().await.unwrap()); 63 | let sdk_config = Arc::new(get_aws_sdk_config().await); 64 | Migrator::refresh(&db_connection).await.unwrap(); 65 | let db_user = DbUser::new(&db_connection); 66 | let repo_user = RepoUser::new(db_user); 67 | let factory = Factory::new(&db_connection, &sdk_config); 68 | let auth_service = Arc::new(factory.new_auth_service()); 69 | let user_service = Arc::new(UserService { 70 | repo_user, 71 | auth_service: auth_service.clone(), 72 | }); 73 | SetupResponse { 74 | user_service, 75 | auth_service, 76 | } 77 | } 78 | 79 | pub struct SetupResponse { 80 | user_service: Arc, 81 | auth_service: Arc, 82 | } 83 | -------------------------------------------------------------------------------- /libs/core/tests/unit_test.rs: -------------------------------------------------------------------------------- 1 | mod unit; 2 | -------------------------------------------------------------------------------- /libs/utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "discuz-utils" 3 | version = "0.1.0" 4 | authors = ["Ken "] 5 | rust-version = "1.65.0" 6 | description = "A set of utils used across the application. " 7 | edition = "2021" 8 | license = "ISC" 9 | keywords = ["discuz", "utils"] 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | async-trait = "0.1.60" 14 | aws-config = "0.52.0" 15 | derive_more = "0.99.17" 16 | dotenv = "0.15.0" 17 | error-stack = "0.2.4" 18 | figment = { version = "0.10.8", features = ["env", "toml"] } 19 | fred = "5.2.0" 20 | jsonwebtoken = "8.2.0" 21 | lazy_static = "1.4.0" 22 | reqwest = { version = "0.11.13", features = ["json"] } 23 | sea-orm = { version = "0.10.5", features = [ 24 | "sqlx-mysql", 25 | "sqlx-sqlite", 26 | "runtime-tokio-native-tls", 27 | ] } 28 | serde = { version = "1.0.151", features = ["derive"] } 29 | serde_derive = "1.0.151" 30 | serde_json = "1.0.91" 31 | tokio = { version = "1", features = ["full"] } 32 | -------------------------------------------------------------------------------- /libs/utils/src/amazon.rs: -------------------------------------------------------------------------------- 1 | use aws_config::{self, SdkConfig}; 2 | use dotenv::dotenv; 3 | 4 | pub async fn get_aws_sdk_config() -> SdkConfig { 5 | dotenv().ok(); 6 | aws_config::load_from_env().await 7 | } 8 | -------------------------------------------------------------------------------- /libs/utils/src/db_connection.rs: -------------------------------------------------------------------------------- 1 | use error_stack::{IntoReport, Result, ResultExt}; 2 | use sea_orm::{Database, DatabaseConnection, DbErr}; 3 | 4 | use crate::config::{get_config, Database as DatabaseConfig}; 5 | 6 | pub async fn get_db_connection() -> Result { 7 | let config = get_config().to_owned(); 8 | let url = get_connection_string(&config.database); 9 | Database::connect(url) 10 | .await 11 | .into_report() 12 | .change_context(DbErr::Custom("Unable to connect the database".to_string())) 13 | } 14 | 15 | pub fn get_connection_string(config: &DatabaseConfig) -> String { 16 | if let Some(db_url) = config.url.to_owned() { 17 | db_url 18 | } else { 19 | let DatabaseConfig { 20 | username, 21 | password, 22 | port, 23 | hostname, 24 | database, 25 | .. 26 | } = config; 27 | format!("mysql://{username}:{password}@{hostname}:{port}/{database}") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /libs/utils/src/http_errors.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde_json; 3 | use std::{ 4 | env::current_dir, 5 | fs::File, 6 | io::{Error as IOError, Read}, 7 | }; 8 | use tokio::sync::Mutex; 9 | 10 | lazy_static! { 11 | static ref HTTP_ERRORS: Mutex>> = Mutex::new(None); 12 | } 13 | 14 | pub async fn get_http_error(code: u32) -> Result, IOError> { 15 | let mut http_errors = get_http_errors().await?.into_iter(); 16 | Ok(http_errors.find(|x| x.code == code)) 17 | } 18 | 19 | pub async fn get_http_errors() -> Result, IOError> { 20 | if let Some(cache) = HTTP_ERRORS.lock().await.clone() { 21 | return Ok(cache); 22 | } 23 | 24 | let mut data = String::new(); 25 | let mut path = current_dir()?; 26 | path.push("dict/http-errors.json"); 27 | File::open(path)?.read_to_string(&mut data)?; 28 | 29 | let errors: Vec = serde_json::from_str(&data)?; 30 | 31 | // Save the http errors into mutex block 32 | let mut lock = HTTP_ERRORS.lock().await; 33 | *lock = Some(errors.clone()); 34 | 35 | Ok(errors) 36 | } 37 | 38 | #[derive(Clone, Deserialize)] 39 | pub struct HttpError { 40 | pub name: String, 41 | pub code: u32, 42 | pub message: String, 43 | #[serde(rename(deserialize = "type"))] 44 | pub error_type: String, 45 | } 46 | -------------------------------------------------------------------------------- /libs/utils/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | 4 | pub mod amazon; 5 | pub mod config; 6 | mod db_connection; 7 | pub mod http_errors; 8 | pub mod redis; 9 | 10 | pub use db_connection::*; 11 | -------------------------------------------------------------------------------- /libs/utils/src/redis.rs: -------------------------------------------------------------------------------- 1 | use crate::config::get_config; 2 | use fred::prelude::*; 3 | use serde::{de::DeserializeOwned, Serialize}; 4 | use tokio::sync::Mutex; 5 | 6 | lazy_static! { 7 | static ref REDIS: Mutex> = Mutex::new(None); 8 | } 9 | 10 | pub async fn get_redis_client() -> Result, RedisError> { 11 | let config = get_config(); 12 | if !config.redis.enable { 13 | return Ok(None); 14 | } 15 | let mut redis = REDIS.lock().await; 16 | if redis.is_none() { 17 | let client = get_redis_connection().await?; 18 | *redis = client; 19 | } 20 | Ok(redis.clone()) 21 | } 22 | 23 | pub async fn get_redis_connection() -> Result, RedisError> { 24 | let config = get_config(); 25 | 26 | if !config.redis.enable { 27 | return Ok(None); 28 | } 29 | 30 | let connection_string = config.redis.get_connection_string(); 31 | 32 | let config = RedisConfig::from_url(&connection_string)?; 33 | let policy = ReconnectPolicy::default(); 34 | let client = RedisClient::new(config); 35 | 36 | let _ = client.connect(Some(policy)); 37 | client.wait_for_connect().await?; 38 | 39 | Ok(Some(client)) 40 | } 41 | 42 | pub async fn get_cached_result(key: &str) -> Option 43 | where 44 | T: DeserializeOwned, 45 | { 46 | if let Some(json_string) = get_stringified_json(key).await { 47 | let json: Option = serde_json::from_str(&json_string).unwrap_or_else(|error| { 48 | println!("{:#?}", error); 49 | None 50 | }); 51 | return json; 52 | } 53 | None 54 | } 55 | 56 | pub async fn set_cached_result(key: &str, json: T, expire: Option) { 57 | if let Ok(json_string) = serde_json::to_string(&json) { 58 | set_stringified_json(key, &json_string, expire).await; 59 | } 60 | } 61 | 62 | pub async fn get_stringified_json(key: &str) -> Option { 63 | if let Ok(Some(redis)) = get_redis_client().await { 64 | let result: Option = redis.get(key).await.unwrap_or(None); 65 | return result; 66 | } 67 | None 68 | } 69 | 70 | pub async fn set_stringified_json(key: &str, value: &str, expire: Option) { 71 | if let Ok(Some(redis)) = get_redis_client().await { 72 | redis 73 | .set(key, value, expire, None, false) 74 | .await 75 | .unwrap_or(()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /libs/utils/tests/unit/db_connection.rs: -------------------------------------------------------------------------------- 1 | // TODO: More test cases should be added to test the ability to get the connection url 2 | 3 | use discuz_utils::{config::Database, get_connection_string}; 4 | 5 | #[test] 6 | fn test_database_connection_url() { 7 | let config = Database { 8 | hostname: "localhost".to_owned(), 9 | username: "mysql".to_owned(), 10 | password: "password".to_owned(), 11 | port: 3306, 12 | database: "database".to_owned(), 13 | url: None, 14 | }; 15 | assert_eq!( 16 | get_connection_string(&config), 17 | "mysql://mysql:password@localhost:3306/database" 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /libs/utils/tests/unit/http_errors.rs: -------------------------------------------------------------------------------- 1 | use discuz_utils::http_errors::{get_http_error, get_http_errors}; 2 | 3 | #[tokio::test] 4 | async fn test_get_http_errors() { 5 | let errors = get_http_errors().await.unwrap(); 6 | assert!(!errors.is_empty()); 7 | } 8 | 9 | #[tokio::test] 10 | async fn test_get_http_error() { 11 | let exist_error = get_http_error(400).await.unwrap(); 12 | assert!(exist_error.is_some()); 13 | let exist_error = exist_error.unwrap(); 14 | assert_eq!(exist_error.code, 400); 15 | assert_eq!(exist_error.error_type, "BadRequest"); 16 | 17 | let non_exist_error = get_http_error(200).await.unwrap(); 18 | assert!(non_exist_error.is_none()); 19 | } 20 | -------------------------------------------------------------------------------- /libs/utils/tests/unit/mod.rs: -------------------------------------------------------------------------------- 1 | mod db_connection; 2 | mod http_errors; 3 | mod redis; 4 | -------------------------------------------------------------------------------- /libs/utils/tests/unit/redis.rs: -------------------------------------------------------------------------------- 1 | use discuz_utils::config::Redis; 2 | 3 | #[test] 4 | fn test_default_redis_url() { 5 | let redis = Redis { 6 | ..Default::default() 7 | }; 8 | assert_eq!(redis.get_connection_string(), "redis://localhost:6379/0"); 9 | } 10 | 11 | #[test] 12 | fn test_redis_url() { 13 | let redis = Redis { 14 | password: Some("123456".to_owned()), 15 | ..Default::default() 16 | }; 17 | assert_eq!( 18 | redis.get_connection_string(), 19 | "redis://:123456@localhost:6379/0" 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /libs/utils/tests/unit_test.rs: -------------------------------------------------------------------------------- 1 | mod unit; 2 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | deploy: 2 | git config credential.helper store 3 | git pull 4 | docker-compose build 5 | docker-compose up -d --remove-orphans 6 | 7 | restart: 8 | docker-compose restart 9 | -------------------------------------------------------------------------------- /notes/「Get」 Threads or 「List」 Threads?.md: -------------------------------------------------------------------------------- 1 | # Get Threads or List Threads 2 | In a thread service, I have implemented a function that retrieves posts and their replies. I am uncertain about the most appropriate function name, get_threads or list_threads. I sought advice from ChatGPT, and this is its answer: 3 | 4 | ### Which function name is more appropriate for retrieving information about threads: get_threads or list_threads? 5 | If the goal is to retrieve the information about a specific number of threads, then get_threads would be the more appropriate name, as it implies a specific, limited retrieval of information. 6 | 7 | On the other hand, if the goal is to retrieve a list of all available threads, then list_threads would be the more appropriate name, as it implies a complete, comprehensive retrieval of information. 8 | 9 | ### Is get_threads a more appropriate function name if the goal is to retrieve information about threads with pagination and filtering capabilities? 10 | Yes, if you need to implement pagination and filtering for retrieving information about threads, get_threads would be a more appropriate name. The name get_threads implies a limited, specific retrieval of information, which is consistent with the idea of pagination and filtering. 11 | 12 | For example, you could have a get_threads function that takes parameters such as page number and filter criteria, and returns the corresponding threads for the given page and filter. This function would provide a more user-friendly and flexible interface for retrieving information about threads, allowing the caller to easily retrieve only the information they need. 13 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | hard_tabs = true 3 | -------------------------------------------------------------------------------- /tests/api-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TEST_COLLECTIONS_LIST=( \ 4 | user_update_profile 5 | ) 6 | 7 | for collection in ${TEST_COLLECTIONS_LIST[@]}; do 8 | newman run ./postman-collections/$collection.json \ 9 | --reporter-cli-no-assertions \ 10 | --reporter-cli-no-success-assertions \ 11 | --reporter-cli-no-console 12 | done 13 | -------------------------------------------------------------------------------- /tests/wait-for-readiness.sh: -------------------------------------------------------------------------------- 1 | set -xe 2 | 3 | wait_for_readiness() { 4 | local SERVICE="$1" 5 | local PORT="$2" 6 | local TRY_TIMEOUT=300 7 | local TRY_INTERVAL=1 8 | local REMAINING_TIME=$TRY_TIMEOUT 9 | while ! curl http://localhost:${PORT}/api/health-check -s --include | head -n1 | grep -q 200; do 10 | REMAINING_TIME=$((REMAINING_TIME - TRY_INTERVAL)) 11 | if [ $REMAINING_TIME -lt 0 ]; then 12 | echo "Error: '${SERVICE}' did not start in expected duration." 13 | exit 1 14 | fi 15 | echo "Waiting for '${SERVICE}' to start... remaning ${REMAINING_TIME} seconds." 16 | sleep $TRY_INTERVAL 17 | done 18 | echo "The '${SERVICE}' is ready to be tested." 19 | } 20 | 21 | wait_for_readiness 'API Server' 3100 22 | --------------------------------------------------------------------------------