Skip to content

Commit

Permalink
feat: Add api-example project
Browse files Browse the repository at this point in the history
  • Loading branch information
yangjing committed Aug 15, 2024
1 parent 39a9b2c commit f5162da
Show file tree
Hide file tree
Showing 40 changed files with 983 additions and 72 deletions.
6 changes: 6 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"recommendations": [
"rust-lang.rust-analyzer",
"fill-labs.dependi",
]
}
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,6 @@
},
"accessibility.signals.positionHasError": {
"sound": "off"
}
},
"editor.tabSize": 2
}
16 changes: 11 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
[workspace]
members = ["ultimates/*", "crates/*", "examples/db-example"]
members = [
"ultimates/*",
"crates/*",
"examples/db-example",
"examples/api-example",
]
resolver = "2"

[workspace.package]
version = "0.1.0"
edition = "2021"
rust-version = "1.79"
description = "Rust libraries"
description = "Rust libraries of The ultimate-common"
license-file = "LICENSE"
repository = "https://gitee.com/yangbajing/ultimate-common"

Expand All @@ -25,7 +30,7 @@ ultimate = { version = "0.1", path = "ultimates/ultimate" }
ultimate-db = { version = "0.1", path = "ultimates/ultimate-db" }
ultimate-web = { version = "0.1", path = "ultimates/ultimate-web" }
# -- projects end
derive_more = { version = "1.0", features = ["from", "display"] }
derive_more = { version = "1.0", features = ["from", "display", "constructor"] }
toml = "0.8"
config = { version = "0.14", default-features = false, features = [
"toml",
Expand Down Expand Up @@ -54,8 +59,7 @@ typed-builder = "0.19"
derive-getters = "0.5"
clap = { version = "4.5.7", features = ["derive"] }
# -- Helpful macros for working with enums and strings
strum = { version = "0.26", features = ["derive"] }
strum_macros = "0.26"
enum-iterator = "2"
# -- Error
anyhow = "1"
thiserror = "1"
Expand Down Expand Up @@ -158,3 +162,5 @@ opendal = { version = "0.48", features = ["services-obs"] }

# build-dependencies
# tonic-build = "0.12"
strum = { version = "0.26", features = ["derive"] }
strum_macros = "0.26"
51 changes: 51 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Examples

## 服务依赖

使用 docker compose 启动并初始化 PG 数据库

```sh
# 当前目录下有容器,先删除容器及数据卷(一般在 sql 有变更时需要)
#docker compose down --volumes --remove-orphans

docker compose up -d --build
```

## api-example

### 启动服务

```sh
cargo run --bin api-example
```

### 测试服务

#### 使用密码登录

```sh
curl -v --location 'http://localhost:8888/auth/login/pwd' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "[email protected]",
"pwd": "2024.Ultimate"
}' | python -m json.tool
```

登录成功返回 token

```sh
{
"token": "eyJ0eXAiOiJKV1QiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..EZwETCBq1CNs8yO5Zec09Q.g3JoMryHoq01ZO3TQ2Ja_ppJZb9SYdon-LfB6OGyH7s.sBCGn14NuoxujmAgRpkYPg",
"token_type": "Bearer"
}
```

#### 用户-分页查询

```sh
curl -v --location 'http://localhost:8888/v1/user/page' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..EZwETCBq1CNs8yO5Zec09Q.g3JoMryHoq01ZO3TQ2Ja_ppJZb9SYdon-LfB6OGyH7s.sBCGn14NuoxujmAgRpkYPg' \
--data '{}' | python -m json.tool --no-ensure-ascii
```
32 changes: 32 additions & 0 deletions examples/api-example/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "api-example"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
description.workspace = true
license-file.workspace = true
repository.workspace = true

[lints]
workspace = true

[dependencies]
ultimate-common = { workspace = true }
ultimate = { workspace = true, features = ["ulid"] }
ultimate-web = { workspace = true }
ultimate-db = { workspace = true }
thiserror.workspace = true
tokio.workspace = true
tower-http.workspace = true
axum.workspace = true
typed-builder.workspace = true
derive-getters.workspace = true
derive_more.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_repr.workspace = true
sqlx.workspace = true
sea-query.workspace = true
sea-query-binder.workspace = true
modql.workspace = true
enum-iterator.workspace = true
1 change: 1 addition & 0 deletions examples/api-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# 使用 Rust 开发高效能的 API 服务
19 changes: 19 additions & 0 deletions examples/api-example/resources/app.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[ultimate.app]
run_mode = "DEV"
name = "app-example"

[ultimate.security.pwd]
expires_in = 604800
default_pwd = "2024.Ultimate"

[ultimate.web]
enable = true
server_addr = "0.0.0.0:8888"

[ultimate.db]
enable = true
host = "localhost"
port = 15432
database = "ultimate"
username = "ultimate"
password = "2024.Ultimate"
42 changes: 42 additions & 0 deletions examples/api-example/src/auth/auth_serv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use axum::{
async_trait,
extract::FromRequestParts,
http::{request::Parts, StatusCode},
};
use derive_more::derive::Constructor;
use ultimate::{security::pwd::verify_pwd, Result};
use ultimate_web::AppError;

use crate::{
state::AppState,
user::{UserFilter, UserServ},
util::make_token,
};

use super::{LoginByPwdReq, LoginResp, TokenType};

#[derive(Constructor)]
pub struct AuthServ {
app: AppState,
}

impl AuthServ {
pub async fn login_by_pwd(&self, req: LoginByPwdReq) -> Result<LoginResp> {
let user_serv = UserServ::new(self.app.clone(), self.app.create_super_admin_ctx());

let (u, uc) = user_serv.get_fetch_credential(UserFilter::from(&req)).await?;
verify_pwd(&req.pwd, &uc.encrypted_pwd).await?;

let token = make_token(self.app.ultimate_config().security(), u.id)?;
Ok(LoginResp { token, token_type: TokenType::Bearer })
}
}

#[async_trait]
impl FromRequestParts<AppState> for AuthServ {
type Rejection = (StatusCode, AppError);

async fn from_request_parts(_parts: &mut Parts, state: &AppState) -> core::result::Result<Self, Self::Rejection> {
Ok(AuthServ::new(state.clone()))
}
}
7 changes: 7 additions & 0 deletions examples/api-example/src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod auth_serv;
mod model;
mod web;

use auth_serv::AuthServ;
use model::*;
pub use web::*;
48 changes: 48 additions & 0 deletions examples/api-example/src/auth/model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use modql::filter::{FilterNodes, OpValString, OpValsString};
use serde::{Deserialize, Serialize};

use crate::user::UserFilter;

#[derive(FilterNodes)]
pub struct LoginFilter {
pub email: Option<OpValsString>,
pub phone: Option<OpValsString>,
}

impl From<&LoginByPwdReq> for LoginFilter {
fn from(req: &LoginByPwdReq) -> Self {
Self {
email: req.email.as_deref().map(|s| OpValString::Eq(s.to_string()).into()),
phone: req.phone.as_deref().map(|s| OpValString::Eq(s.to_string()).into()),
}
}
}

#[derive(Deserialize)]
pub struct LoginByPwdReq {
pub email: Option<String>,
pub phone: Option<String>,
#[serde(skip_serializing)]
pub pwd: String,
}

impl From<&LoginByPwdReq> for UserFilter {
fn from(value: &LoginByPwdReq) -> Self {
UserFilter {
email: value.email.as_deref().map(|s| OpValString::Eq(s.to_string()).into()),
phone: value.phone.as_deref().map(|s| OpValString::Eq(s.to_string()).into()),
..Default::default()
}
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct LoginResp {
pub token: String,
pub token_type: TokenType,
}

#[derive(Debug, Serialize, Deserialize)]
pub enum TokenType {
Bearer,
}
15 changes: 15 additions & 0 deletions examples/api-example/src/auth/web.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use axum::{routing::post, Json, Router};
use ultimate_web::{ok, AppResult};

use crate::state::AppState;

use super::{AuthServ, LoginByPwdReq, LoginResp};

pub fn auth_routes() -> Router<AppState> {
Router::new().route("/login/pwd", post(login_pwd))
}

async fn login_pwd(auth_serv: AuthServ, Json(req): Json<LoginByPwdReq>) -> AppResult<LoginResp> {
let resp = auth_serv.login_by_pwd(req).await?;
ok(resp)
}
66 changes: 66 additions & 0 deletions examples/api-example/src/ctx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::sync::Arc;

use axum::{
async_trait,
extract::FromRequestParts,
http::{request::Parts, HeaderMap, StatusCode},
Json,
};
use derive_getters::Getters;
use ultimate::ctx::Ctx;
use ultimate_db::ModelManager;
use ultimate_web::{extract_session, AppError};

use crate::state::AppState;

static X_APP_VERSION: &str = "X-APP-VARSION";
static X_DEVICE_ID: &str = "X-DEVICE-ID";

#[derive(Clone, Getters)]
pub struct CtxW {
ctx: Ctx,
mm: ModelManager,
req_meta: Arc<RequestMetadata>,
}
impl CtxW {
pub fn new(state: &AppState, ctx: Ctx, req_meta: Arc<RequestMetadata>) -> Self {
let mm = state.mm().clone().with_ctx(ctx.clone());
Self { ctx, mm, req_meta }
}
}

#[async_trait]
impl FromRequestParts<AppState> for CtxW {
type Rejection = (StatusCode, Json<AppError>);

async fn from_request_parts(parts: &mut Parts, state: &AppState) -> core::result::Result<Self, Self::Rejection> {
match extract_session(parts, state.ultimate_config().security()) {
Ok(ctx) => Ok(CtxW::new(state, ctx, Arc::new(RequestMetadata::from(&parts.headers)))),
Err(e) => Err((StatusCode::UNAUTHORIZED, Json(e.into()))),
}
}
}

#[derive(Clone, Default)]
pub struct RequestMetadata {
app_ver: String,
dev_id: String,
}

impl RequestMetadata {
pub fn app_ver(&self) -> &str {
self.app_ver.as_str()
}

pub fn dev_id(&self) -> &str {
self.dev_id.as_str()
}
}

impl From<&HeaderMap> for RequestMetadata {
fn from(headers: &HeaderMap) -> Self {
let app_ver = headers.get(X_APP_VERSION).map(|v| v.to_str().unwrap_or("").to_string()).unwrap_or_default();
let dev_id = headers.get(X_DEVICE_ID).map(|v| v.to_str().unwrap_or("").to_string()).unwrap_or_default();
Self { app_ver, dev_id }
}
}
6 changes: 6 additions & 0 deletions examples/api-example/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mod auth;
pub mod ctx;
pub mod router;
pub mod state;
mod user;
mod util;
12 changes: 12 additions & 0 deletions examples/api-example/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use api_example::{router::new_api_router, state::new_app_state};
use ultimate_web::server::init_server;

#[tokio::main]
async fn main() -> ultimate::Result<()> {
let state = new_app_state().await?;
let conf = state.ultimate_config();
let router = new_api_router(state.clone());

init_server(conf, router).await?;
Ok(())
}
7 changes: 7 additions & 0 deletions examples/api-example/src/router.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use axum::Router;

use crate::{auth::auth_routes, state::AppState, user::user_routes};

pub fn new_api_router(app_state: AppState) -> Router {
Router::new().nest("/v1/user", user_routes()).nest("/auth", auth_routes()).with_state(app_state)
}
Loading

0 comments on commit f5162da

Please sign in to comment.