forked from photino/zino
Add the ntex integration
This commit is contained in:
parent
132ef1a62b
commit
ed55703978
|
@ -18,11 +18,11 @@ which emphasizes **simplicity**, **extensibility** and **productivity**.
|
|||
- 📅 Lightweight scheduler for sync and async cron jobs.
|
||||
- 💠 Unified access to storage services, data sources and chatbots.
|
||||
- 📊 Built-in support for [`tracing`], [`metrics`] and logging.
|
||||
- 💖 Full integrations with [`actix-web`], [`axum`] and [`dioxus`].
|
||||
- 💖 Full integrations with [`actix-web`], [`axum`], [`dioxus`] and [`ntex`].
|
||||
|
||||
## Getting started
|
||||
|
||||
You can start with the example [`actix-app`], [`axum-app`] or [`dioxus-desktop`].
|
||||
You can start with the example [`actix-app`], [`axum-app`], [`dioxus-desktop`] or [`ntex-app`].
|
||||
It requires **Rust 1.75+** to build the project.
|
||||
|
||||
```shell
|
||||
|
@ -38,7 +38,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
zino = { version = "0.20", features = ["axum"] }
|
||||
zino = { version = "0.21", features = ["axum"] }
|
||||
```
|
||||
|
||||
```rust
|
||||
|
@ -94,8 +94,10 @@ If you have any problems or ideas, please don't hesitate to [open an issue][zino
|
|||
[`actix-web`]: https://crates.io/crates/actix-web
|
||||
[`axum`]: https://crates.io/crates/axum
|
||||
[`dioxus`]: https://crates.io/crates/dioxus
|
||||
[`ntex`]: https://crates.io/crates/ntex
|
||||
[`actix-app`]: https://github.com/zino-rs/zino/tree/main/examples/actix-app
|
||||
[`axum-app`]: https://github.com/zino-rs/zino/tree/main/examples/axum-app
|
||||
[`dioxus-desktop`]: https://github.com/zino-rs/zino/tree/main/examples/dioxus-desktop
|
||||
[`ntex-app`]: https://github.com/zino-rs/zino/tree/main/examples/ntex-app
|
||||
[license]: https://github.com/zino-rs/zino/blob/main/LICENSE
|
||||
[zino-issue]: https://github.com/zino-rs/zino/issues/new
|
||||
|
|
|
@ -5,6 +5,7 @@ members = [
|
|||
"axum-app",
|
||||
"dioxus-desktop",
|
||||
"minimal-app",
|
||||
"ntex-app",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "actix-app"
|
||||
description = "An example for actix-web integration."
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
@ -19,7 +19,7 @@ features = ["derive"]
|
|||
|
||||
[dependencies.zino]
|
||||
path = "../../zino"
|
||||
version = "0.20.3"
|
||||
version = "0.21.0"
|
||||
features = [
|
||||
"actix",
|
||||
"i18n",
|
||||
|
@ -29,7 +29,7 @@ features = [
|
|||
|
||||
[dependencies.zino-core]
|
||||
path = "../../zino-core"
|
||||
version = "0.21.3"
|
||||
version = "0.22.0"
|
||||
features = [
|
||||
"cookie",
|
||||
"env-filter",
|
||||
|
@ -39,8 +39,8 @@ features = [
|
|||
|
||||
[dependencies.zino-derive]
|
||||
path = "../../zino-derive"
|
||||
version = "0.18.3"
|
||||
version = "0.19.0"
|
||||
|
||||
[dependencies.zino-model]
|
||||
path = "../../zino-model"
|
||||
version = "0.18.3"
|
||||
version = "0.19.0"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "axum-app"
|
||||
description = "An example for axum integration."
|
||||
version = "0.13.1"
|
||||
version = "0.13.2"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
@ -19,7 +19,7 @@ features = ["derive"]
|
|||
|
||||
[dependencies.zino]
|
||||
path = "../../zino"
|
||||
version = "0.20.3"
|
||||
version = "0.21.0"
|
||||
features = [
|
||||
"axum",
|
||||
"i18n",
|
||||
|
@ -29,7 +29,7 @@ features = [
|
|||
|
||||
[dependencies.zino-core]
|
||||
path = "../../zino-core"
|
||||
version = "0.21.3"
|
||||
version = "0.22.0"
|
||||
features = [
|
||||
"cookie",
|
||||
"env-filter",
|
||||
|
@ -40,8 +40,8 @@ features = [
|
|||
|
||||
[dependencies.zino-derive]
|
||||
path = "../../zino-derive"
|
||||
version = "0.18.3"
|
||||
version = "0.19.0"
|
||||
|
||||
[dependencies.zino-model]
|
||||
path = "../../zino-model"
|
||||
version = "0.18.3"
|
||||
version = "0.19.0"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "dioxus-desktop"
|
||||
description = "An example for Dioxus desktop integration."
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
@ -26,18 +26,18 @@ features = ["derive"]
|
|||
|
||||
[dependencies.zino]
|
||||
path = "../../zino"
|
||||
version = "0.20.3"
|
||||
version = "0.21.0"
|
||||
features = ["dioxus-desktop", "orm"]
|
||||
|
||||
[dependencies.zino-core]
|
||||
path = "../../zino-core"
|
||||
version = "0.21.3"
|
||||
version = "0.22.0"
|
||||
features = ["env-filter", "orm-sqlite", "tls-rustls"]
|
||||
|
||||
[dependencies.zino-model]
|
||||
path = "../../zino-model"
|
||||
version = "0.18.3"
|
||||
version = "0.19.0"
|
||||
|
||||
[dependencies.zino-dioxus]
|
||||
path = "../../zino-dioxus"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
[package]
|
||||
name = "minimal-app"
|
||||
description = "A minimal example to run a server."
|
||||
version = "0.1.3"
|
||||
version = "0.2.0"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies.zino]
|
||||
path = "../../zino"
|
||||
version = "0.20.3"
|
||||
version = "0.21.0"
|
||||
features = ["axum"]
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
[package]
|
||||
name = "ntex-app"
|
||||
description = "An example for ntex integration."
|
||||
version = "0.1.0"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1.40"
|
||||
|
||||
[dependencies.ntex]
|
||||
version = "1.2.1"
|
||||
default-features = false
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.198"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.zino]
|
||||
path = "../../zino"
|
||||
version = "0.21.0"
|
||||
features = [
|
||||
"ntex",
|
||||
"i18n",
|
||||
"jwt",
|
||||
"orm",
|
||||
]
|
||||
|
||||
[dependencies.zino-core]
|
||||
path = "../../zino-core"
|
||||
version = "0.22.0"
|
||||
features = [
|
||||
"cookie",
|
||||
"env-filter",
|
||||
"orm-postgres",
|
||||
"view-minijinja",
|
||||
]
|
||||
|
||||
[dependencies.zino-derive]
|
||||
path = "../../zino-derive"
|
||||
version = "0.19.0"
|
||||
|
||||
[dependencies.zino-model]
|
||||
path = "../../zino-model"
|
||||
version = "0.19.0"
|
|
@ -0,0 +1,5 @@
|
|||
# ntex-app
|
||||
|
||||
This folder provides an example for the integration with [`ntex`].
|
||||
|
||||
[`ntex`]: https://crates.io/crates/ntex
|
|
@ -0,0 +1,54 @@
|
|||
# --env=dev
|
||||
|
||||
name = "data-cube"
|
||||
version = "0.6.4"
|
||||
|
||||
[dirs]
|
||||
uploads = "local/uploads"
|
||||
|
||||
[debug]
|
||||
host = "127.0.0.1"
|
||||
port = 6070
|
||||
|
||||
[main]
|
||||
host = "127.0.0.1"
|
||||
port = 6080
|
||||
|
||||
[[standby]]
|
||||
host = "127.0.0.1"
|
||||
port = 6081
|
||||
tag = "portal"
|
||||
|
||||
[[standby]]
|
||||
host = "127.0.0.1"
|
||||
port = 6082
|
||||
tag = "admin"
|
||||
|
||||
[database]
|
||||
namespace = "dc"
|
||||
max-rows = 10000
|
||||
|
||||
[[postgres]]
|
||||
host = "127.0.0.1"
|
||||
port = 5432
|
||||
database = "data_cube"
|
||||
username = "postgres"
|
||||
password = "smcddNr2mrpwgYvO6ICRLPFfLFd27WySGN9a7a9JrsYP3tIP"
|
||||
|
||||
[[sqlite]]
|
||||
database = "local/data/main.db"
|
||||
|
||||
[tracing]
|
||||
filter = "info,sqlx=info,zino=trace,zino_core=trace"
|
||||
|
||||
[metrics]
|
||||
exporter = "prometheus"
|
||||
host = "127.0.0.1"
|
||||
port = 9000
|
||||
|
||||
[jwt]
|
||||
max-age = "20m"
|
||||
refresh-interval = "7d"
|
||||
|
||||
[openapi]
|
||||
custom-html = "local/docs/rapidoc.html"
|
|
@ -0,0 +1,51 @@
|
|||
# --env=prod
|
||||
|
||||
name = "data-cube"
|
||||
version = "0.6.4"
|
||||
|
||||
[dirs]
|
||||
uploads = "local/uploads"
|
||||
|
||||
[debug]
|
||||
host = "127.0.0.1"
|
||||
port = 6070
|
||||
|
||||
[main]
|
||||
host = "127.0.0.1"
|
||||
port = 6080
|
||||
|
||||
[[standby]]
|
||||
host = "127.0.0.1"
|
||||
port = 6081
|
||||
tag = "portal"
|
||||
|
||||
[[standby]]
|
||||
host = "127.0.0.1"
|
||||
port = 6082
|
||||
tag = "admin"
|
||||
|
||||
[database]
|
||||
namespace = "dc"
|
||||
|
||||
[[postgres]]
|
||||
host = "127.0.0.1"
|
||||
port = 5432
|
||||
database = "data_cube"
|
||||
username = "postgres"
|
||||
password = "uzU7eGgSw6HLp63qbjpMQMnl4Kk0SrDDp9bIXkRmcjqwRFRf"
|
||||
|
||||
[[sqlite]]
|
||||
database = "local/data/main.db"
|
||||
|
||||
[tracing]
|
||||
level = "warn"
|
||||
|
||||
[metrics]
|
||||
exporter = "prometheus"
|
||||
host = "127.0.0.1"
|
||||
port = 9000
|
||||
|
||||
[openapi]
|
||||
show-docs = true
|
||||
rapidoc-route = "/rapidoc"
|
||||
custom-html = "local/docs/rapidoc.html"
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
## User
|
||||
user-intro = Welcome, { $name }!
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
## User
|
||||
user-intro = 欢迎{ $name }!
|
|
@ -0,0 +1,18 @@
|
|||
[info]
|
||||
title = "DataCube API"
|
||||
description = """
|
||||
An example for [actix-web] integration.
|
||||
|
||||
[actix-web]: https://crates.io/crates/actix-web
|
||||
"""
|
||||
contact = { url = "https://github.com/zino-rs/zino" }
|
||||
license = "MIT"
|
||||
version = "1.0"
|
||||
|
||||
[security_schemes.jwt_auth]
|
||||
type = "http"
|
||||
scheme = "bearer"
|
||||
bearer_format = "JWT"
|
||||
|
||||
[[securities]]
|
||||
name = "jwt_auth"
|
|
@ -0,0 +1,22 @@
|
|||
name = "Authentication"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/auth/login"
|
||||
method = "POST"
|
||||
summary = "Logins the user account"
|
||||
securities = []
|
||||
|
||||
[endpoints.body]
|
||||
type = "object"
|
||||
account = { type = "string", description = "User account" }
|
||||
password = { type = "string", format = "password", description = "User password" }
|
||||
|
||||
[[endpoints]]
|
||||
path = "/auth/refresh"
|
||||
method = "GET"
|
||||
summary = "Refreshes the access token"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/auth/logout"
|
||||
method = "POST"
|
||||
summary = "Logouts the user account"
|
|
@ -0,0 +1,22 @@
|
|||
name = "Files"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/file/upload"
|
||||
method = "POST"
|
||||
summary = "Uploads the file"
|
||||
|
||||
[endpoints.body]
|
||||
content_type = "multipart/form-data"
|
||||
required = ["file"]
|
||||
file = { type = "string", format = "binary", description = "File content" }
|
||||
|
||||
[[endpoints]]
|
||||
path = "/file/decrypt"
|
||||
method = "GET"
|
||||
summary = "Decrypts the file"
|
||||
|
||||
[endpoints.query]
|
||||
required = ["file_name", "access_key_id", "security_token"]
|
||||
file_name = { type = "string", description = "File name" }
|
||||
access_key_id = { type = "string", description = "Access key ID" }
|
||||
security_token = { type = "string", description = "Security token" }
|
|
@ -0,0 +1,56 @@
|
|||
name = "Tags"
|
||||
securities = []
|
||||
|
||||
[[endpoints]]
|
||||
path = "/tag/new"
|
||||
method = "POST"
|
||||
summary = "Creates a new tag"
|
||||
|
||||
[endpoints.body]
|
||||
schema = "/tag/definition"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/tag/{tag_id}/delete"
|
||||
method = "POST"
|
||||
summary = "Deletes a tag by ID"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/tag/{tag_id}/update"
|
||||
method = "POST"
|
||||
summary = "Updates a tag by ID"
|
||||
|
||||
[endpoints.body]
|
||||
schema = "/tag/definition?action=update"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/tag/{tag_id}/view"
|
||||
method = "GET"
|
||||
summary = "Gets a tag by ID"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/tag/list"
|
||||
method = "GET"
|
||||
summary = "Finds a list of tags"
|
||||
|
||||
[endpoints.query]
|
||||
category = { type = "string", description = "Tag category" }
|
||||
parent_id = { schema = "tagId" }
|
||||
|
||||
[schemas.tagId]
|
||||
type = "string"
|
||||
format = "uuid"
|
||||
description = "Tag ID"
|
||||
|
||||
[models.tag.visibility]
|
||||
translations = [
|
||||
["Public", "🌐"],
|
||||
["Internal", "🔵"],
|
||||
["Proteched", "⛔"],
|
||||
["Private", "🔴"],
|
||||
]
|
||||
|
||||
[models.tag.status]
|
||||
translations = [
|
||||
["Active", "😄"],
|
||||
["Inactive", "😴"],
|
||||
]
|
|
@ -0,0 +1,113 @@
|
|||
name = "Users"
|
||||
securities = []
|
||||
|
||||
[[endpoints]]
|
||||
path = "/user/new"
|
||||
method = "POST"
|
||||
summary = "Creates a new user"
|
||||
|
||||
[endpoints.body]
|
||||
schema = "newUser"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/user/{user_id}/delete"
|
||||
method = "POST"
|
||||
summary = "Deletes a user by ID"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/user/{user_id}/update"
|
||||
method = "POST"
|
||||
summary = "Updates a user by ID"
|
||||
|
||||
[endpoints.body]
|
||||
schema = "userInfo"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/user/{user_id}/view"
|
||||
method = "GET"
|
||||
summary = "Gets a user by ID"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/user/list"
|
||||
method = "GET"
|
||||
summary = "Finds a list of users"
|
||||
|
||||
[endpoints.query]
|
||||
roles = { type = "string", description = "User roles" }
|
||||
tags = { type = "string", description = "User tags" }
|
||||
|
||||
[[endpoints]]
|
||||
path = "/user/import"
|
||||
method = "POST"
|
||||
summary = "Imports the user data"
|
||||
|
||||
[endpoints.body]
|
||||
schema = "userData"
|
||||
|
||||
[[endpoints]]
|
||||
path = "/user/export"
|
||||
method = "GET"
|
||||
summary = "Exports the user data"
|
||||
|
||||
[endpoints.query]
|
||||
format = { type = "string", enum = ["csv", "json", "jsonlines"], default = "json", description = "File format" }
|
||||
roles = { type = "string", description = "User roles" }
|
||||
tags = { type = "string", description = "User tags" }
|
||||
|
||||
[schemas.userId]
|
||||
type = "string"
|
||||
format = "uuid"
|
||||
description = "User ID"
|
||||
|
||||
[schemas.newUser]
|
||||
type = "object"
|
||||
required = ["name", "roles", "account", "password"]
|
||||
name = { type = "string", description = "User name" }
|
||||
roles = { type = "array", items = "string", example = ["admin"], description = "User roles" }
|
||||
account = { type = "string", description = "User account" }
|
||||
password = { type = "string", format = "password", description = "User password" }
|
||||
tags = { type = "array", items = { type = "string", format = "uuid" }, description = "User tags" }
|
||||
|
||||
[schemas.userInfo]
|
||||
type = "object"
|
||||
name = { type = "string", description = "User name" }
|
||||
status = { type = "string", enum = ["Active", "Inactive", "Locked", "Deleted"], description = "User status" }
|
||||
roles = { type = "array", items = "string", description = "User roles" }
|
||||
tags = { type = "array", items = { type = "string", format = "uuid" }, description = "User tags" }
|
||||
|
||||
[schemas.userData]
|
||||
type = "array"
|
||||
items = "object"
|
||||
required = ["name", "roles", "account", "password"]
|
||||
name = { type = "string", description = "User name" }
|
||||
roles = { type = "array", items = "string", example = ["admin"], description = "User roles" }
|
||||
account = { type = "string", description = "User account" }
|
||||
password = { type = "string", format = "password", description = "User password" }
|
||||
tags = { type = "array", items = { type = "string", format = "uuid" }, description = "User tags" }
|
||||
|
||||
[models.user.visibility]
|
||||
translations = [
|
||||
["Public", "🌐"],
|
||||
["Internal", "🔵"],
|
||||
["Proteched", "⛔"],
|
||||
["Private", "🔴"],
|
||||
]
|
||||
|
||||
[models.user.status]
|
||||
translations = [
|
||||
["Active", "😄"],
|
||||
["Inactive", "😴"],
|
||||
]
|
||||
|
||||
[models.user.roles]
|
||||
translations = [
|
||||
["admin", "👮"],
|
||||
["worker", "👷"],
|
||||
]
|
||||
|
||||
[models.user.updated_at]
|
||||
translations = [
|
||||
["$span:24h", "Updated within 1 day"],
|
||||
["$span:7d", "Updated within 1 week"],
|
||||
]
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{"timestamp":"2023-02-22T13:30:37.2454056+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:9000","exporter":"prometheus"},"target":"zino_core::application::metrics_exporter"}
|
||||
{"timestamp":"2023-02-22T13:30:37.4556548+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6080","env":"dev"},"target":"zino::cluster::axum_cluster"}
|
||||
{"timestamp":"2023-02-22T13:30:37.4573445+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6081","env":"dev"},"target":"zino::cluster::axum_cluster"}
|
||||
{"timestamp":"2023-02-22T13:30:37.4589684+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6082","env":"dev"},"target":"zino::cluster::axum_cluster"}
|
||||
{"timestamp":"2023-02-22T13:30:44.3227123+08:00","level":"DEBUG","fields":{"message":"started processing request"},"target":"zino::middleware::tower_tracing","span":{"context.span_id":"1","http.method":"GET","http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
||||
{"timestamp":"2023-02-22T13:30:44.7291153+08:00","level":"INFO","fields":{"message":"finished processing request"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"7c73a4af-c256-4e96-a335-84bf72b0e0a9","context.span_id":"1","context.trace_id":"4278ab19-a395-45a5-8122-941319f2039b","http.method":"GET","http.response.header.traceparent":"00-4278ab19a39545a58122941319f2039b-0000000000000001-03","http.response.header.tracestate":"zino=1","http.server.duration":406,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
||||
{"timestamp":"2023-02-22T13:30:44.7295366+08:00","level":"DEBUG","fields":{"message":"flushed 1944 bytes"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"7c73a4af-c256-4e96-a335-84bf72b0e0a9","context.span_id":"1","context.trace_id":"4278ab19-a395-45a5-8122-941319f2039b","http.method":"GET","http.response.header.traceparent":"00-4278ab19a39545a58122941319f2039b-0000000000000001-03","http.response.header.tracestate":"zino=1","http.server.duration":406,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
||||
{"timestamp":"2023-02-22T13:31:43.8217876+08:00","level":"DEBUG","fields":{"message":"started processing request"},"target":"zino::middleware::tower_tracing","span":{"context.span_id":"8000000001","http.method":"GET","http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
||||
{"timestamp":"2023-02-22T13:31:43.8331486+08:00","level":"INFO","fields":{"message":"finished processing request"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"da7d4993-0ec6-44e2-b42e-70ff3ac781d0","context.span_id":"8000000001","context.trace_id":"beb53cc8-33c5-4dcc-9a50-3c855136774a","http.method":"GET","http.response.header.traceparent":"00-beb53cc833c54dcc9a503c855136774a-0000008000000001-03","http.response.header.tracestate":"zino=8000000001","http.server.duration":11,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
||||
{"timestamp":"2023-02-22T13:31:43.8336681+08:00","level":"DEBUG","fields":{"message":"flushed 1944 bytes"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"da7d4993-0ec6-44e2-b42e-70ff3ac781d0","context.span_id":"8000000001","context.trace_id":"beb53cc8-33c5-4dcc-9a50-3c855136774a","http.method":"GET","http.response.header.traceparent":"00-beb53cc833c54dcc9a503c855136774a-0000008000000001-03","http.response.header.tracestate":"zino=8000000001","http.server.duration":11,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
|
@ -0,0 +1,14 @@
|
|||
"id","name","namespace","visibility","status","description","access_key_id","account","password","mobile","email","avatar","roles","tags","content","metrics","extra","owner_id","maintainer_id","created_at","updated_at","version","edition"
|
||||
"b3ba6584-3e22-47fc-bd47-770c1d1c55c0","alice","dc:user","internal","active","","SsuJAujkjQ8pr1ntIfQY","","","","","","{admin}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 18:34:01.4012+08","2022-12-16 18:34:01.401206+08",0,0
|
||||
"2fc3c930-0d99-46e5-a1b2-8f3d6b61771e","bob","dc:user","internal","active","","uiaTnFjGwY9uJc5RbPZZ","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 18:34:34.316352+08","2022-12-16 18:34:34.316372+08",0,0
|
||||
"67c96d3f-e020-4ed5-8608-6234abd29063","carol","dc:user","internal","active","","kHUze4mBkaQH4mwIfRBT","","","","","","{admin,worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 18:46:28.686262+08","2022-12-16 18:46:28.68628+08",0,0
|
||||
"06fdaacd-3d02-48eb-9f7c-64349451dce4","dave","dc:user","internal","active","","d1l2hlKrOA1twJKWep6X","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 18:46:43.224728+08","2022-12-16 18:46:43.224736+08",0,0
|
||||
"316de34b-88ba-4f08-bf50-27449de28dd5","eve","dc:user","internal","active","","MqtTxBQq28QcK6cNxZaA","","","","","","{auditor}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 18:47:05.391224+08","2022-12-16 18:47:05.391273+08",0,0
|
||||
"620ac06f-b2b3-45b6-8509-bc6692b9d22d","frid","dc:user","internal","active","","LwgWVfdW59oyYCHEjJkQ","","","","","","{auditor,worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 19:29:57.196914+08","2022-12-16 19:29:57.196921+08",0,0
|
||||
"cad561d5-ff6e-4323-bdb1-ce304cf7fd08","grace","dc:user","internal","active","","kQ1vvrdnAK5gQI0rJxtD","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2022-12-16 19:30:11.000058+08","2022-12-16 19:30:11.000065+08",0,0
|
||||
"329000bb-f36d-477b-9fcc-4d728e6b63b9","tom","dc:user","internal","active","","nSIUIMMNHtCBr2Bd1v5f","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-01-27 15:34:37.770765+08","2023-01-27 15:34:37.770777+08",0,0
|
||||
"0b8b59c3-fe72-45a8-b9e9-f670d2cfb2ca","peter","dc:user","internal","active","","b3VJCpJWH3kpo9hc1MNZ","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-01-27 15:52:14.224398+08","2023-01-27 15:52:14.224408+08",0,0
|
||||
"dbb56a21-7e6b-4702-b069-1f337645a8e6","peter","dc:user","internal","active","","qsOHvYfJQALId5QEazzh","","","","","","{worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-01-27 16:59:25.898107+08","2023-01-27 16:59:25.898155+08",0,0
|
||||
"0eb846c2-7c09-4a7b-b7c8-78092f1a85e1","zino","dc:user","internal","active","","6kimxTOU0f3ysxqUKDgW","","","","","","{worker,auditor}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-02-18 21:09:42.466218+08","2023-02-18 21:09:42.466256+08",0,0
|
||||
"f944a630-3677-4559-9de2-8723e9c26d08","zino","dc:user","internal","active","","6DI5DP8dbuNvTHnMA07r","","","","","","{worker,auditor}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-02-18 21:38:23.270831+08","2023-02-18 21:38:23.270865+08",0,0
|
||||
"b5b261ed-b1c1-46a3-88c7-a22b9eb76b40","bob","dc:user","internal","active","","XXcwVoahGQrsDdlG7s73","","","","","","{admin,worker}","{}","{}","{}","{}","00000000-0000-0000-0000-000000000000","00000000-0000-0000-0000-000000000000","2023-02-18 21:42:13.0111+08","2023-02-18 21:42:13.011111+08",0,0
|
|
|
@ -0,0 +1,21 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>RapiDoc</title>
|
||||
<script type="module" src="https://s4.zstatic.net/npm/rapidoc/dist/rapidoc-min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<rapi-doc spec-url="$specUrl"
|
||||
heading-text="RapiDoc"
|
||||
schema-style="table"
|
||||
schema-description-expanded="true"
|
||||
default-schema-tab="schema"
|
||||
show-method-in-nav-bar="as-colored-block"
|
||||
sort-endpoints-by="method"
|
||||
persist-auth="true"
|
||||
allow-spec-file-download="true"
|
||||
primary-color="#2d87e2">
|
||||
</rapi-doc>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>404 Not Found</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<h3>404 Not Found</h3>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,10 @@
|
|||
{"timestamp":"2023-02-22T13:30:37.2454056+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:9000","exporter":"prometheus"},"target":"zino_core::application::metrics_exporter"}
|
||||
{"timestamp":"2023-02-22T13:30:37.4556548+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6080","env":"dev"},"target":"zino::cluster::axum_cluster"}
|
||||
{"timestamp":"2023-02-22T13:30:37.4573445+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6081","env":"dev"},"target":"zino::cluster::axum_cluster"}
|
||||
{"timestamp":"2023-02-22T13:30:37.4589684+08:00","level":"WARN","fields":{"message":"listen on 127.0.0.1:6082","env":"dev"},"target":"zino::cluster::axum_cluster"}
|
||||
{"timestamp":"2023-02-22T13:30:44.3227123+08:00","level":"DEBUG","fields":{"message":"started processing request"},"target":"zino::middleware::tower_tracing","span":{"context.span_id":"1","http.method":"GET","http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
||||
{"timestamp":"2023-02-22T13:30:44.7291153+08:00","level":"INFO","fields":{"message":"finished processing request"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"7c73a4af-c256-4e96-a335-84bf72b0e0a9","context.span_id":"1","context.trace_id":"4278ab19-a395-45a5-8122-941319f2039b","http.method":"GET","http.response.header.traceparent":"00-4278ab19a39545a58122941319f2039b-0000000000000001-03","http.response.header.tracestate":"zino=1","http.server.duration":406,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
||||
{"timestamp":"2023-02-22T13:30:44.7295366+08:00","level":"DEBUG","fields":{"message":"flushed 1944 bytes"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"7c73a4af-c256-4e96-a335-84bf72b0e0a9","context.span_id":"1","context.trace_id":"4278ab19-a395-45a5-8122-941319f2039b","http.method":"GET","http.response.header.traceparent":"00-4278ab19a39545a58122941319f2039b-0000000000000001-03","http.response.header.tracestate":"zino=1","http.server.duration":406,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
||||
{"timestamp":"2023-02-22T13:31:43.8217876+08:00","level":"DEBUG","fields":{"message":"started processing request"},"target":"zino::middleware::tower_tracing","span":{"context.span_id":"8000000001","http.method":"GET","http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
||||
{"timestamp":"2023-02-22T13:31:43.8331486+08:00","level":"INFO","fields":{"message":"finished processing request"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"da7d4993-0ec6-44e2-b42e-70ff3ac781d0","context.span_id":"8000000001","context.trace_id":"beb53cc8-33c5-4dcc-9a50-3c855136774a","http.method":"GET","http.response.header.traceparent":"00-beb53cc833c54dcc9a503c855136774a-0000008000000001-03","http.response.header.tracestate":"zino=8000000001","http.server.duration":11,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
||||
{"timestamp":"2023-02-22T13:31:43.8336681+08:00","level":"DEBUG","fields":{"message":"flushed 1944 bytes"},"target":"zino::middleware::tower_tracing","span":{"context.request_id":"da7d4993-0ec6-44e2-b42e-70ff3ac781d0","context.span_id":"8000000001","context.trace_id":"beb53cc8-33c5-4dcc-9a50-3c855136774a","http.method":"GET","http.response.header.traceparent":"00-beb53cc833c54dcc9a503c855136774a-0000008000000001-03","http.response.header.tracestate":"zino=8000000001","http.server.duration":11,"http.status_code":200,"http.target":"/user/b3ba6584-3e22-47fc-bd47-770c1d1c55c0/view","http.user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50","otel.kind":"server","otel.name":"data-cube","name":"HTTP request"}}
|
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Index</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<h3>Congratulations, you have booted your application successfully!</h3>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>File Uploader</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<form method="POST" action="/file/upload" enctype="multipart/form-data">
|
||||
<div>Title: <input name="title"/></div>
|
||||
<div>File: <input type="file" name="files" multiple/></div>
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,59 @@
|
|||
use zino::{prelude::*, Request, Response, Result};
|
||||
use zino_model::user::{JwtAuthService, User};
|
||||
|
||||
pub async fn login(mut req: Request) -> Result {
|
||||
let current_time = DateTime::now();
|
||||
let body: Map = req.parse_body().await?;
|
||||
let (user_id, mut data) = User::generate_token(body).await.extract(&req)?;
|
||||
|
||||
let user_updates = json!({
|
||||
"status": "Active",
|
||||
"last_login_at": data.remove("current_login_at").and_then(|v| v.as_datetime()),
|
||||
"last_login_ip": data.remove("current_login_ip"),
|
||||
"current_login_at": current_time,
|
||||
"current_login_ip": req.client_ip(),
|
||||
"$inc": { "login_count": 1 },
|
||||
});
|
||||
|
||||
let mut user_mutations = user_updates.into_map_opt().unwrap_or_default();
|
||||
let (validation, user) = User::update_by_id(&user_id, &mut user_mutations, None)
|
||||
.await
|
||||
.extract(&req)?;
|
||||
if !validation.is_success() {
|
||||
reject!(req, validation);
|
||||
}
|
||||
data.upsert("entry", user.snapshot());
|
||||
|
||||
let mut res = Response::default().context(&req);
|
||||
res.set_json_data(data);
|
||||
Ok(res.into())
|
||||
}
|
||||
|
||||
pub async fn refresh(req: Request) -> Result {
|
||||
let claims = req.parse_jwt_claims(JwtClaims::shared_key())?;
|
||||
let data = User::refresh_token(&claims).await.extract(&req)?;
|
||||
let mut res = Response::default().context(&req);
|
||||
res.set_json_data(data);
|
||||
Ok(res.into())
|
||||
}
|
||||
|
||||
pub async fn logout(req: Request) -> Result {
|
||||
let user_session = req
|
||||
.get_data::<UserSession<_>>()
|
||||
.ok_or_else(|| warn!("401 Unauthorized: the user session is invalid"))
|
||||
.extract(&req)?;
|
||||
|
||||
let mut mutations = Map::from_entry("status", "SignedOut");
|
||||
let user_id = user_session.user_id();
|
||||
let (validation, user) = User::update_by_id(user_id, &mut mutations, None)
|
||||
.await
|
||||
.extract(&req)?;
|
||||
if !validation.is_success() {
|
||||
reject!(req, validation);
|
||||
}
|
||||
|
||||
let data = Map::data_entry(user.snapshot());
|
||||
let mut res = Response::default().context(&req);
|
||||
res.set_json_data(data);
|
||||
Ok(res.into())
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
use std::time::{Duration, Instant};
|
||||
use zino::{prelude::*, Cluster, Request, Response, Result};
|
||||
|
||||
pub async fn upload(mut req: Request) -> Result {
|
||||
let (mut body, files) = req.parse_form_data::<Map>().await?;
|
||||
|
||||
let dir = Cluster::shared_dir("uploads");
|
||||
let expires = DateTime::now() + Duration::from_secs(600);
|
||||
let mut encryption_duration = Duration::ZERO;
|
||||
let mut uploads = Vec::new();
|
||||
for mut file in files {
|
||||
let mut query = Map::new();
|
||||
let access_key_id = AccessKeyId::new();
|
||||
query.upsert("access_key_id", access_key_id.to_string());
|
||||
|
||||
let secret_key = SecretAccessKey::new(&access_key_id);
|
||||
let security_token =
|
||||
SecurityToken::try_new(access_key_id, expires, &secret_key).extract(&req)?;
|
||||
query.upsert("security_token", security_token.to_string());
|
||||
|
||||
let encryption_start_time = Instant::now();
|
||||
file.encrypt_with(secret_key.as_ref()).extract(&req)?;
|
||||
encryption_duration += encryption_start_time.elapsed();
|
||||
|
||||
if let Some(file_name) = file.file_name() {
|
||||
file.write(dir.join(file_name)).extract(&req)?;
|
||||
query.upsert("file_name", file_name);
|
||||
|
||||
let mut map = Map::new();
|
||||
map.upsert("field_name", file.field_name());
|
||||
map.upsert("file_name", file_name);
|
||||
map.upsert("content_type", file.content_type().map(|m| m.as_ref()));
|
||||
map.upsert("url", format!("/file/decrypt?{}", query.to_query_string()));
|
||||
uploads.push(map);
|
||||
}
|
||||
}
|
||||
body.upsert("files", uploads);
|
||||
|
||||
let mut res = Response::default().context(&req);
|
||||
res.record_server_timing("enc", None, Some(encryption_duration));
|
||||
res.set_json_data(Map::data_entry(body));
|
||||
Ok(res.into())
|
||||
}
|
||||
|
||||
pub async fn decrypt(req: Request) -> Result {
|
||||
let query = req.parse_query::<Map>()?;
|
||||
let access_key_id = req.parse_access_key_id()?;
|
||||
let secret_key = SecretAccessKey::new(&access_key_id);
|
||||
let security_token = req.parse_security_token(secret_key.as_ref())?;
|
||||
if security_token.is_expired() {
|
||||
reject!(req, forbidden, "the security token has expired");
|
||||
}
|
||||
|
||||
let Some(file_name) = query.get_str("file_name") else {
|
||||
reject!(req, "file_name", "it should be specified");
|
||||
};
|
||||
let file_path = Cluster::shared_dir("uploads").join(file_name);
|
||||
|
||||
let mut file = NamedFile::try_from_local(file_path).extract(&req)?;
|
||||
let decryption_start_time = Instant::now();
|
||||
file.decrypt_with(secret_key).extract(&req)?;
|
||||
|
||||
let decryption_duration = decryption_start_time.elapsed();
|
||||
let mut res = Response::default().context(&req);
|
||||
res.record_server_timing("dec", None, Some(decryption_duration));
|
||||
res.send_file(file);
|
||||
Ok(res.into())
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
pub(crate) mod auth;
|
||||
pub(crate) mod file;
|
||||
pub(crate) mod stats;
|
||||
pub(crate) mod user;
|
|
@ -0,0 +1,15 @@
|
|||
use zino::{prelude::*, Cluster, Request, Response, Result};
|
||||
|
||||
pub async fn index(req: Request) -> Result {
|
||||
let res = Response::default().context(&req);
|
||||
let stats = json!({
|
||||
"method": "GET",
|
||||
"path": "/stats",
|
||||
"app_state_data": Cluster::state_data(),
|
||||
});
|
||||
let data = json!({
|
||||
"title": "Stats",
|
||||
"output": stats.to_string_pretty(),
|
||||
});
|
||||
Ok(res.render("output.html", data).into())
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
use std::time::Instant;
|
||||
use zino::{prelude::*, Request, Response, Result};
|
||||
use zino_model::user::User;
|
||||
|
||||
pub async fn new(mut req: Request) -> Result {
|
||||
let mut user = User::new();
|
||||
let mut res = req.model_validation(&mut user).await?;
|
||||
let validation = user.check_constraints().await.extract(&req)?;
|
||||
if !validation.is_success() {
|
||||
reject!(req, validation);
|
||||
}
|
||||
|
||||
let user_name = user.name().to_owned();
|
||||
user.insert().await.extract(&req)?;
|
||||
|
||||
let args = fluent_args![
|
||||
"name" => user_name
|
||||
];
|
||||
let user_intro = req.translate("user-intro", Some(args)).extract(&req)?;
|
||||
let data = json!({
|
||||
"method": req.request_method().as_ref(),
|
||||
"path": req.request_path(),
|
||||
"user_intro": user_intro,
|
||||
});
|
||||
let locale = req.new_cookie("locale".into(), "en-US".into(), None);
|
||||
res.set_cookie(&locale);
|
||||
res.set_code(StatusCode::CREATED);
|
||||
res.set_json_data(data);
|
||||
Ok(res.into())
|
||||
}
|
||||
|
||||
pub async fn view(req: Request) -> Result {
|
||||
let user_id = req.parse_param("id")?;
|
||||
|
||||
let db_query_start_time = Instant::now();
|
||||
let user = User::fetch_by_id(&user_id).await.extract(&req)?;
|
||||
let db_query_duration = db_query_start_time.elapsed();
|
||||
|
||||
let data = Map::data_entry(user);
|
||||
let mut res = Response::default().context(&req);
|
||||
res.record_server_timing("db", None, Some(db_query_duration));
|
||||
res.set_json_data(data);
|
||||
Ok(res.into())
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
mod controller;
|
||||
mod domain;
|
||||
mod extension;
|
||||
mod logic;
|
||||
mod middleware;
|
||||
mod model;
|
||||
mod router;
|
||||
mod schedule;
|
||||
mod service;
|
||||
|
||||
use zino::prelude::*;
|
||||
|
||||
fn main() {
|
||||
zino::Cluster::boot()
|
||||
.register(router::routes())
|
||||
.register_debug(router::debug_routes())
|
||||
.spawn(schedule::job_scheduler())
|
||||
.run_with(schedule::async_job_scheduler())
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
use ntex::{
|
||||
service::{Middleware, Service, ServiceCtx},
|
||||
web::{Error, ErrorRenderer, WebRequest, WebResponse},
|
||||
};
|
||||
use zino::{prelude::*, Request};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UserSessionInitializer;
|
||||
|
||||
pub struct UserSessionMiddleware<S> {
|
||||
service: S,
|
||||
}
|
||||
|
||||
impl<S> Middleware<S> for UserSessionInitializer {
|
||||
type Service = UserSessionMiddleware<S>;
|
||||
|
||||
fn create(&self, service: S) -> Self::Service {
|
||||
UserSessionMiddleware { service }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, Err> Service<WebRequest<Err>> for UserSessionMiddleware<S>
|
||||
where
|
||||
S: Service<WebRequest<Err>, Response = WebResponse, Error = Error>,
|
||||
Err: ErrorRenderer,
|
||||
{
|
||||
type Response = WebResponse;
|
||||
type Error = Error;
|
||||
|
||||
ntex::forward_poll_ready!(service);
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
req: WebRequest<Err>,
|
||||
ctx: ServiceCtx<'_, Self>,
|
||||
) -> Result<Self::Response, Self::Error> {
|
||||
let mut req = Request::from(req);
|
||||
match req.parse_jwt_claims(JwtClaims::shared_key()) {
|
||||
Ok(claims) => {
|
||||
if let Ok(mut user_session) = UserSession::<Uuid>::try_from_jwt_claims(claims) {
|
||||
if let Ok(session_id) = req.parse_session_id() {
|
||||
user_session.set_session_id(session_id);
|
||||
}
|
||||
req.set_data(user_session);
|
||||
} else {
|
||||
let message = "401 Unauthorized: invalid JWT claims";
|
||||
let rejection = Rejection::with_message(message).context(&req).into();
|
||||
let result: zino::Result<Self::Response> = Err(rejection);
|
||||
return result.map_err(|err| err.into());
|
||||
}
|
||||
}
|
||||
Err(rejection) => {
|
||||
let result: zino::Result<Self::Response> = Err(rejection.into());
|
||||
return result.map_err(|err| err.into());
|
||||
}
|
||||
}
|
||||
|
||||
let req = WebRequest::try_from(req)?;
|
||||
let res = ctx.call(&self.service, req).await?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
mod access;
|
||||
|
||||
pub(crate) use access::UserSessionInitializer;
|
|
@ -0,0 +1,3 @@
|
|||
mod tag;
|
||||
|
||||
pub(crate) use tag::Tag;
|
|
@ -0,0 +1,50 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use zino::prelude::*;
|
||||
use zino_derive::{DecodeRow, Model, ModelAccessor, ModelHooks, Schema};
|
||||
|
||||
/// The `tag` model.
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
DecodeRow,
|
||||
Schema,
|
||||
ModelAccessor,
|
||||
ModelHooks,
|
||||
Model,
|
||||
)]
|
||||
#[serde(default)]
|
||||
pub struct Tag {
|
||||
// Basic fields.
|
||||
#[schema(primary_key, read_only, constructor = "Uuid::now_v7")]
|
||||
id: Uuid,
|
||||
#[schema(not_null, comment = "Tag name")]
|
||||
name: String,
|
||||
#[schema(default_value = "Active", index_type = "hash")]
|
||||
status: String,
|
||||
description: String,
|
||||
|
||||
// Info fields.
|
||||
#[schema(not_null, index_type = "hash", comment = "Tag category")]
|
||||
category: String,
|
||||
#[schema(
|
||||
snapshot,
|
||||
reference = "Tag",
|
||||
fetch_as = "parent_tag",
|
||||
comment = "Optional parent tag"
|
||||
)]
|
||||
parent_id: Option<Uuid>,
|
||||
|
||||
// Extensions.
|
||||
#[schema(reserved)]
|
||||
extra: Map,
|
||||
|
||||
// Revisions.
|
||||
#[schema(read_only, default_value = "now", index_type = "btree")]
|
||||
created_at: DateTime,
|
||||
#[schema(default_value = "now", index_type = "btree")]
|
||||
updated_at: DateTime,
|
||||
version: u64,
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
use crate::{
|
||||
controller::{auth, file, stats, user},
|
||||
middleware,
|
||||
model::Tag,
|
||||
};
|
||||
use ntex::web::{get, post, scope, ServiceConfig};
|
||||
use zino::{DefaultController, RouterConfigure};
|
||||
use zino_model::User;
|
||||
|
||||
pub fn routes() -> Vec<RouterConfigure> {
|
||||
vec![
|
||||
auth_router as RouterConfigure,
|
||||
file_router as RouterConfigure,
|
||||
user_router as RouterConfigure,
|
||||
tag_router as RouterConfigure,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn debug_routes() -> Vec<RouterConfigure> {
|
||||
vec![
|
||||
stats_router as RouterConfigure,
|
||||
user_debug_router as RouterConfigure,
|
||||
tag_debug_router as RouterConfigure,
|
||||
]
|
||||
}
|
||||
|
||||
fn auth_router(cfg: &mut ServiceConfig) {
|
||||
cfg.route("/auth/login", post().to(auth::login));
|
||||
cfg.service(
|
||||
scope("/auth")
|
||||
.route("/refresh", get().to(auth::refresh))
|
||||
.route("/logout", post().to(auth::logout))
|
||||
.wrap(middleware::UserSessionInitializer),
|
||||
);
|
||||
}
|
||||
|
||||
fn file_router(cfg: &mut ServiceConfig) {
|
||||
cfg.service(
|
||||
scope("/file")
|
||||
.route("/upload", post().to(file::upload))
|
||||
.route("/decrypt", get().to(file::decrypt))
|
||||
.wrap(middleware::UserSessionInitializer),
|
||||
);
|
||||
}
|
||||
|
||||
fn user_router(cfg: &mut ServiceConfig) {
|
||||
cfg.route("/user/new", post().to(user::new))
|
||||
.route("/user/{id}/delete", post().to(User::soft_delete))
|
||||
.route("/user/{id}/update", post().to(User::update))
|
||||
.route("/user/{id}/view", get().to(user::view))
|
||||
.route("/user/list", get().to(User::list))
|
||||
.route("/user/import", post().to(User::import))
|
||||
.route("/user/export", get().to(User::export));
|
||||
}
|
||||
|
||||
fn tag_router(cfg: &mut ServiceConfig) {
|
||||
cfg.route("/tag/new", post().to(Tag::new))
|
||||
.route("/tag/{id}/delete", post().to(Tag::soft_delete))
|
||||
.route("/tag/{id}/update", post().to(Tag::update))
|
||||
.route("/tag/{id}/view", get().to(Tag::view))
|
||||
.route("/tag/list", get().to(Tag::list))
|
||||
.route("/tag/tree", get().to(Tag::tree));
|
||||
}
|
||||
|
||||
fn stats_router(cfg: &mut ServiceConfig) {
|
||||
cfg.route("/stats", get().to(stats::index));
|
||||
}
|
||||
|
||||
fn user_debug_router(cfg: &mut ServiceConfig) {
|
||||
cfg.route("/user/schema", get().to(User::schema))
|
||||
.route("/user/definition", get().to(User::definition))
|
||||
.route("/user/mock", get().to(User::mock));
|
||||
}
|
||||
|
||||
fn tag_debug_router(cfg: &mut ServiceConfig) {
|
||||
cfg.route("/tag/schema", get().to(Tag::schema))
|
||||
.route("/tag/definition", get().to(Tag::definition))
|
||||
.route("/tag/mock", get().to(Tag::mock));
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
use zino::prelude::*;
|
||||
use zino_model::User;
|
||||
|
||||
pub fn every_15s(job_id: Uuid, job_data: &mut Map, last_tick: DateTime) {
|
||||
let counter = job_data
|
||||
.get("counter")
|
||||
.map(|c| c.as_u64().unwrap_or_default() + 1)
|
||||
.unwrap_or_default();
|
||||
job_data.upsert("counter", counter);
|
||||
job_data.upsert("current", DateTime::now());
|
||||
job_data.upsert("last_tick", last_tick);
|
||||
job_data.upsert("job_id", job_id.to_string());
|
||||
}
|
||||
|
||||
pub fn every_20s(job_id: Uuid, job_data: &mut Map, last_tick: DateTime) {
|
||||
let counter = job_data
|
||||
.get("counter")
|
||||
.map(|c| c.as_u64().unwrap_or_default() + 1)
|
||||
.unwrap_or_default();
|
||||
job_data.upsert("counter", counter);
|
||||
job_data.upsert("current", DateTime::now());
|
||||
job_data.upsert("last_tick", last_tick);
|
||||
job_data.upsert("job_id", job_id.to_string());
|
||||
}
|
||||
|
||||
pub fn every_hour(job_id: Uuid, job_data: &mut Map, last_tick: DateTime) -> BoxFuture {
|
||||
let counter = job_data
|
||||
.get("counter")
|
||||
.map(|c| c.as_u64().unwrap_or_default() + 1)
|
||||
.unwrap_or_default();
|
||||
job_data.upsert("counter", counter);
|
||||
job_data.upsert("current", DateTime::now());
|
||||
job_data.upsert("last_tick", last_tick);
|
||||
job_data.upsert("job_id", job_id.to_string());
|
||||
Box::pin(async {
|
||||
let query = Query::default();
|
||||
let columns = [("*", true), ("roles", true)];
|
||||
if let Ok(mut map) = User::count_many(&query, &columns).await {
|
||||
job_data.append(&mut map);
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
use zino::prelude::*;
|
||||
|
||||
mod job;
|
||||
|
||||
pub fn job_scheduler() -> JobScheduler {
|
||||
let mut scheduler = JobScheduler::new();
|
||||
|
||||
let job = Job::new("0/15 * * * * *", job::every_15s as CronJob).disable(true);
|
||||
scheduler.add(job);
|
||||
|
||||
let job = Job::new("0/20 * * * * *", job::every_20s as CronJob).max_ticks(3);
|
||||
scheduler.add(job);
|
||||
|
||||
scheduler
|
||||
}
|
||||
|
||||
pub fn async_job_scheduler() -> AsyncJobScheduler {
|
||||
let mut scheduler = AsyncJobScheduler::new();
|
||||
|
||||
let job = AsyncJob::new("0/25 * * * * *", job::every_hour as AsyncCronJob).immediate(true);
|
||||
scheduler.add(job);
|
||||
|
||||
scheduler
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>{{ title }} | {{ APP_NAME }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% block content %}{% endblock content %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,6 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<div class="output">
|
||||
<code><pre>{{ output }}</pre></code>
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "zino-cli"
|
||||
description = "CLI tools for zino."
|
||||
version = "0.2.4"
|
||||
version = "0.3.0"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
@ -25,4 +25,4 @@ features = ["color", "derive"]
|
|||
|
||||
[dependencies.zino-core]
|
||||
path = "../zino-core"
|
||||
version = "0.21.3"
|
||||
version = "0.22.0"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "zino-core"
|
||||
description = "Core types and traits for zino."
|
||||
version = "0.21.3"
|
||||
version = "0.22.0"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
|
|
@ -15,7 +15,6 @@ use crate::{
|
|||
validation::Validation,
|
||||
warn, JsonValue, Map, SharedString, Uuid,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use multer::Multipart;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::{borrow::Cow, net::IpAddr, str::FromStr, time::Instant};
|
||||
|
@ -84,7 +83,7 @@ pub trait RequestContext {
|
|||
fn client_ip(&self) -> Option<IpAddr>;
|
||||
|
||||
/// Reads the entire request body into a byte buffer.
|
||||
async fn read_body_bytes(&mut self) -> Result<Bytes, Error>;
|
||||
async fn read_body_bytes(&mut self) -> Result<Vec<u8>, Error>;
|
||||
|
||||
/// Returns the request path regardless of nesting.
|
||||
#[inline]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "zino-derive"
|
||||
description = "Derived traits for zino."
|
||||
version = "0.18.3"
|
||||
version = "0.19.0"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
@ -21,5 +21,5 @@ syn = "2.0.60"
|
|||
|
||||
[dependencies.zino-core]
|
||||
path = "../zino-core"
|
||||
version = "0.21.3"
|
||||
version = "0.22.0"
|
||||
features = ["orm"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "zino-dioxus"
|
||||
description = "Dioxus components for zino."
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
@ -38,4 +38,4 @@ features = [
|
|||
|
||||
[dependencies.zino-core]
|
||||
path = "../zino-core"
|
||||
version = "0.21.3"
|
||||
version = "0.22.0"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "zino-extra"
|
||||
description = "Extra utilities for zino."
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
@ -42,4 +42,4 @@ optional = true
|
|||
|
||||
[dependencies.zino-core]
|
||||
path = "../zino-core"
|
||||
version = "0.21.3"
|
||||
version = "0.22.0"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "zino-model"
|
||||
description = "Domain models for zino."
|
||||
version = "0.18.3"
|
||||
version = "0.19.0"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
@ -49,7 +49,7 @@ features = ["derive"]
|
|||
|
||||
[dependencies.zino-core]
|
||||
path = "../zino-core"
|
||||
version = "0.21.3"
|
||||
version = "0.22.0"
|
||||
features = [
|
||||
"jwt",
|
||||
"orm",
|
||||
|
@ -59,4 +59,4 @@ features = [
|
|||
|
||||
[dependencies.zino-derive]
|
||||
path = "../zino-derive"
|
||||
version = "0.18.3"
|
||||
version = "0.19.0"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "zino"
|
||||
description = "Next-generation framework for composable applications in Rust."
|
||||
version = "0.20.3"
|
||||
version = "0.21.0"
|
||||
rust-version = "1.75"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
@ -52,6 +52,12 @@ dioxus-desktop = [
|
|||
default = []
|
||||
i18n = ["zino-core/i18n"]
|
||||
jwt = ["zino-core/jwt"]
|
||||
ntex = [
|
||||
"dep:futures",
|
||||
"dep:ntex",
|
||||
"dep:ntex-files",
|
||||
"zino-core/runtime-tokio",
|
||||
]
|
||||
orm = ["zino-core/orm"]
|
||||
|
||||
[dependencies]
|
||||
|
@ -103,6 +109,16 @@ optional = true
|
|||
version = "0.25.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.ntex]
|
||||
version = "1.2.1"
|
||||
optional = true
|
||||
default-features = false
|
||||
features = ["compress", "tokio"]
|
||||
|
||||
[dependencies.ntex-files]
|
||||
version = "1.0.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.37.0"
|
||||
optional = true
|
||||
|
@ -119,7 +135,7 @@ optional = true
|
|||
features = ["timeout"]
|
||||
|
||||
[dependencies.tower-http]
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
optional = true
|
||||
features = [
|
||||
"add-extension",
|
||||
|
@ -146,4 +162,4 @@ optional = true
|
|||
|
||||
[dependencies.zino-core]
|
||||
path = "../zino-core"
|
||||
version = "0.21.3"
|
||||
version = "0.22.0"
|
||||
|
|
|
@ -20,11 +20,11 @@ which emphasizes **simplicity**, **extensibility** and **productivity**.
|
|||
- 📅 Lightweight scheduler for sync and async cron jobs.
|
||||
- 💠 Unified access to storage services, data sources and chatbots.
|
||||
- 📊 Built-in support for [`tracing`], [`metrics`] and logging.
|
||||
- 💖 Full integrations with [`actix-web`], [`axum`] and [`dioxus`].
|
||||
- 💖 Full integrations with [`actix-web`], [`axum`], [`dioxus`] and [`ntex`].
|
||||
|
||||
## Getting started
|
||||
|
||||
You can start with the example [`actix-app`], [`axum-app`] or [`dioxus-desktop`].
|
||||
You can start with the example [`actix-app`], [`axum-app`], [`dioxus-desktop`] or [`ntex-app`].
|
||||
|
||||
Here is the simplest application to run a server:
|
||||
```toml
|
||||
|
@ -34,7 +34,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
zino = { version = "0.20", features = ["axum"] }
|
||||
zino = { version = "0.21", features = ["axum"] }
|
||||
```
|
||||
|
||||
```rust,ignore
|
||||
|
@ -56,6 +56,7 @@ The following optional features are available:
|
|||
| `dioxus` | Enables the integration with [`dioxus`]. | No |
|
||||
| `i18n` | Enables the support for internationalization. | No |
|
||||
| `jwt` | Enables the support for JSON Web Token. | No |
|
||||
| `ntex` | Enables the integration with [`ntex`]. | No |
|
||||
| `orm` | Enables the ORM for MySQL, PostgreSQL or **SQLite**. | No |
|
||||
|
||||
[`zino`]: https://github.com/zino-rs/zino
|
||||
|
@ -65,6 +66,8 @@ The following optional features are available:
|
|||
[`actix-web`]: https://crates.io/crates/actix-web
|
||||
[`axum`]: https://crates.io/crates/axum
|
||||
[`dioxus`]: https://crates.io/crates/dioxus
|
||||
[`ntex`]: https://crates.io/crates/ntex
|
||||
[`actix-app`]: https://github.com/zino-rs/zino/tree/main/examples/actix-app
|
||||
[`axum-app`]: https://github.com/zino-rs/zino/tree/main/examples/axum-app
|
||||
[`dioxus-desktop`]: https://github.com/zino-rs/zino/tree/main/examples/dioxus-desktop
|
||||
[`ntex-app`]: https://github.com/zino-rs/zino/tree/main/examples/ntex-app
|
||||
|
|
|
@ -13,6 +13,11 @@ cfg_if::cfg_if! {
|
|||
mod plugin_loader;
|
||||
pub(crate) mod dioxus_desktop;
|
||||
|
||||
use plugin_loader::load_plugins;
|
||||
} else if #[cfg(feature = "ntex")] {
|
||||
mod plugin_loader;
|
||||
pub(crate) mod ntex_cluster;
|
||||
|
||||
use plugin_loader::load_plugins;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
use crate::RouterConfigure;
|
||||
use ntex::{
|
||||
rt::System,
|
||||
time::{self, Seconds},
|
||||
web::{
|
||||
self,
|
||||
middleware::Compress,
|
||||
types::{FormConfig, JsonConfig, PayloadConfig},
|
||||
App, HttpServer,
|
||||
},
|
||||
};
|
||||
use ntex_files::{Files, NamedFile};
|
||||
use std::path::PathBuf;
|
||||
use zino_core::{
|
||||
application::{Application, Plugin, ServerTag},
|
||||
extension::TomlTableExt,
|
||||
schedule::AsyncScheduler,
|
||||
};
|
||||
|
||||
/// An HTTP server cluster for `ntex`.
|
||||
#[derive(Default)]
|
||||
pub struct NtexCluster {
|
||||
/// Custom plugins.
|
||||
custom_plugins: Vec<Plugin>,
|
||||
/// Default routes.
|
||||
default_routes: Vec<RouterConfigure>,
|
||||
/// Tagged routes.
|
||||
tagged_routes: Vec<(ServerTag, Vec<RouterConfigure>)>,
|
||||
}
|
||||
|
||||
impl Application for NtexCluster {
|
||||
type Routes = Vec<RouterConfigure>;
|
||||
|
||||
fn register(mut self, routes: Self::Routes) -> Self {
|
||||
self.default_routes = routes;
|
||||
self
|
||||
}
|
||||
|
||||
fn register_with(mut self, server_tag: ServerTag, routes: Self::Routes) -> Self {
|
||||
self.tagged_routes.push((server_tag, routes));
|
||||
self
|
||||
}
|
||||
|
||||
fn add_plugin(mut self, plugin: Plugin) -> Self {
|
||||
self.custom_plugins.push(plugin);
|
||||
self
|
||||
}
|
||||
|
||||
fn run_with<T: AsyncScheduler + Send + 'static>(self, mut scheduler: T) {
|
||||
let app_env = Self::env();
|
||||
System::new("prelude").block_on(async {
|
||||
Self::load().await;
|
||||
super::load_plugins(self.custom_plugins, app_env).await;
|
||||
});
|
||||
if scheduler.is_ready() {
|
||||
System::new("scheduler")
|
||||
.system()
|
||||
.arbiter()
|
||||
.spawn(Box::pin(async move {
|
||||
loop {
|
||||
scheduler.tick().await;
|
||||
|
||||
// Cannot use `std::thread::sleep` because it blocks the Tokio runtime.
|
||||
time::sleep(scheduler.time_till_next_job()).await;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
System::new("main").block_on(async {
|
||||
let default_routes = self.default_routes.leak() as &'static [_];
|
||||
let tagged_routes = self.tagged_routes.leak() as &'static [_];
|
||||
let app_state = Self::shared_state();
|
||||
let app_name = Self::name();
|
||||
let app_version = Self::version();
|
||||
let app_domain = Self::domain();
|
||||
let listeners = app_state.listeners();
|
||||
let servers = listeners.into_iter().map(|listener| {
|
||||
let server_tag = listener.0;
|
||||
let addr = listener.1;
|
||||
tracing::warn!(
|
||||
server_tag = server_tag.as_str(),
|
||||
app_env = app_env.as_str(),
|
||||
app_name,
|
||||
app_version,
|
||||
"listen on `{addr}`",
|
||||
);
|
||||
|
||||
// Server config
|
||||
let project_dir = Self::project_dir();
|
||||
let default_public_dir = project_dir.join("public");
|
||||
let mut public_route_prefix = "/public";
|
||||
let mut public_dir = PathBuf::new();
|
||||
let mut backlog = 2048; // Maximum number of pending connections
|
||||
let mut max_connections = 25000; // Maximum number of concurrent connections
|
||||
let mut body_limit = 128 * 1024 * 1024; // 128MB
|
||||
let mut request_timeout = 60; // 60 seconds
|
||||
if let Some(config) = app_state.get_config("server") {
|
||||
if let Some(dir) = config.get_str("page-dir") {
|
||||
public_route_prefix = "/page";
|
||||
public_dir.push(dir);
|
||||
} else if let Some(dir) = config.get_str("public-dir") {
|
||||
public_dir.push(dir);
|
||||
} else {
|
||||
public_dir = default_public_dir;
|
||||
}
|
||||
if let Some(route_prefix) = config.get_str("public-route-prefix") {
|
||||
public_route_prefix = route_prefix;
|
||||
}
|
||||
if let Some(value) = config.get_i32("backlog") {
|
||||
backlog = value;
|
||||
}
|
||||
if let Some(value) = config.get_usize("max-connections") {
|
||||
max_connections = value;
|
||||
}
|
||||
if let Some(limit) = config.get_usize("body-limit") {
|
||||
body_limit = limit;
|
||||
}
|
||||
if let Some(timeout) = config
|
||||
.get_duration("request-timeout")
|
||||
.and_then(|d| d.as_secs().try_into().ok())
|
||||
{
|
||||
request_timeout = timeout;
|
||||
}
|
||||
} else {
|
||||
public_dir = default_public_dir;
|
||||
}
|
||||
|
||||
HttpServer::new(move || {
|
||||
let mut app = App::new();
|
||||
if public_dir.exists() {
|
||||
let index_file = public_dir.join("index.html");
|
||||
let favicon_file = public_dir.join("favicon.ico");
|
||||
if index_file.exists() {
|
||||
let index_file_handler = web::get()
|
||||
.to(move || async { NamedFile::open("./public/index.html") });
|
||||
app = app.route("/", index_file_handler);
|
||||
}
|
||||
if favicon_file.exists() {
|
||||
let favicon_file_handler =
|
||||
web::get().to(|| async { NamedFile::open("./public/favicon.ico") });
|
||||
app = app.route("/favicon.ico", favicon_file_handler);
|
||||
}
|
||||
|
||||
let static_files = Files::new(public_route_prefix, public_dir.clone())
|
||||
.show_files_listing()
|
||||
.index_file("index.html");
|
||||
app = app.service(static_files);
|
||||
tracing::info!(
|
||||
"Static pages `{public_route_prefix}/**` are registered for `{addr}`"
|
||||
);
|
||||
}
|
||||
for route in default_routes {
|
||||
app = app.configure(route);
|
||||
}
|
||||
for (tag, routes) in tagged_routes {
|
||||
if tag == &server_tag || server_tag.is_debug() {
|
||||
for route in routes {
|
||||
app = app.configure(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.state(FormConfig::default().limit(body_limit))
|
||||
.state(JsonConfig::default().limit(body_limit))
|
||||
.state(PayloadConfig::default().limit(body_limit))
|
||||
.wrap(Compress::default())
|
||||
})
|
||||
.stop_runtime()
|
||||
.disable_signals()
|
||||
.server_hostname(app_domain)
|
||||
.backlog(backlog)
|
||||
.maxconn(max_connections)
|
||||
.client_timeout(Seconds(request_timeout))
|
||||
.bind(addr)
|
||||
.unwrap_or_else(|err| panic!("fail to create an HTTP server: {err}"))
|
||||
.run()
|
||||
});
|
||||
for result in futures::future::join_all(servers).await {
|
||||
if let Err(err) = result {
|
||||
tracing::error!("ntex server error: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ pub trait DefaultController<K, U> {
|
|||
async fn mock(req: Self::Request) -> Self::Result;
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "actix", feature = "axum"))]
|
||||
#[cfg(any(feature = "actix", feature = "axum", feature = "ntex"))]
|
||||
#[cfg(feature = "orm")]
|
||||
use zino_core::{
|
||||
extension::JsonObjectExt,
|
||||
|
@ -63,7 +63,7 @@ use zino_core::{
|
|||
JsonValue, Map,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "actix", feature = "axum"))]
|
||||
#[cfg(any(feature = "actix", feature = "axum", feature = "ntex"))]
|
||||
#[cfg(feature = "orm")]
|
||||
impl<K, U, M> DefaultController<K, U> for M
|
||||
where
|
||||
|
|
|
@ -56,5 +56,24 @@ cfg_if::cfg_if! {
|
|||
|
||||
/// Desktop applications for `dioxus`.
|
||||
pub type Desktop<R> = DioxusDesktop<R>;
|
||||
} else if #[cfg(feature = "ntex")] {
|
||||
use crate::application::ntex_cluster::NtexCluster;
|
||||
use crate::request::ntex_request::NtexExtractor;
|
||||
use crate::response::ntex_response::{NtexRejection, NtexResponse};
|
||||
|
||||
/// HTTP server cluster for `ntex`.
|
||||
pub type Cluster = NtexCluster;
|
||||
|
||||
/// Router configure for `ntex`.
|
||||
pub type RouterConfigure = fn(cfg: &mut ntex::web::ServiceConfig);
|
||||
|
||||
/// A specialized request extractor for `ntex`.
|
||||
pub type Request = NtexExtractor<ntex::web::HttpRequest>;
|
||||
|
||||
/// A specialized response for `ntex`.
|
||||
pub type Response = zino_core::response::Response<ntex::http::StatusCode>;
|
||||
|
||||
/// A specialized `Result` type for `ntex`.
|
||||
pub type Result<T = NtexResponse> = std::result::Result<T, NtexRejection>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,9 +95,9 @@ impl RequestContext for ActixExtractor<HttpRequest> {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
async fn read_body_bytes(&mut self) -> Result<Bytes, Error> {
|
||||
async fn read_body_bytes(&mut self) -> Result<Vec<u8>, Error> {
|
||||
let bytes = Bytes::from_request(&self.0, &mut self.1).await?;
|
||||
Ok(bytes)
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
extract::{ConnectInfo, FromRequest, MatchedPath, OriginalUri, Request},
|
||||
http::{HeaderMap, Method},
|
||||
};
|
||||
|
@ -117,10 +116,10 @@ impl RequestContext for AxumExtractor<Request> {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
async fn read_body_bytes(&mut self) -> Result<Bytes, Error> {
|
||||
async fn read_body_bytes(&mut self) -> Result<Vec<u8>, Error> {
|
||||
let body = mem::take(self.body_mut());
|
||||
let bytes = axum::body::to_bytes(body, usize::MAX).await?;
|
||||
Ok(bytes)
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,5 +3,7 @@ cfg_if::cfg_if! {
|
|||
pub(crate) mod actix_request;
|
||||
} else if #[cfg(feature = "axum")] {
|
||||
pub(crate) mod axum_request;
|
||||
} else if #[cfg(feature = "ntex")] {
|
||||
pub(crate) mod ntex_request;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
use crate::response::ntex_response::NtexRejection;
|
||||
use ntex::{
|
||||
http::{header::HeaderMap, Method, Payload},
|
||||
util::Bytes,
|
||||
web::{
|
||||
error::{DefaultError, ErrorRenderer},
|
||||
FromRequest, HttpRequest, WebRequest,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
convert::Infallible,
|
||||
net::IpAddr,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use zino_core::{
|
||||
error::Error,
|
||||
request::{Context, RequestContext, Uri},
|
||||
response::Rejection,
|
||||
state::Data,
|
||||
};
|
||||
|
||||
/// An HTTP request extractor for `ntex`.
|
||||
pub struct NtexExtractor<T>(T, Payload);
|
||||
|
||||
impl<T> Deref for NtexExtractor<T> {
|
||||
type Target = T;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for NtexExtractor<T> {
|
||||
#[inline]
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestContext for NtexExtractor<HttpRequest> {
|
||||
type Method = Method;
|
||||
type Headers = HeaderMap;
|
||||
|
||||
#[inline]
|
||||
fn request_method(&self) -> &Self::Method {
|
||||
self.method()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn original_uri(&self) -> &Uri {
|
||||
self.uri()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn matched_route(&self) -> Cow<'_, str> {
|
||||
self.match_info().path().into()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn header_map(&self) -> &Self::Headers {
|
||||
self.headers()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_header(&self, name: &str) -> Option<&str> {
|
||||
self.headers().get(name)?.to_str().ok()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_context(&self) -> Option<Context> {
|
||||
let extensions = self.extensions();
|
||||
extensions.get::<Context>().cloned()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_data<T: Clone + Send + Sync + 'static>(&self) -> Option<T> {
|
||||
self.extensions().get::<Data<T>>().map(|data| data.get())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_data<T: Clone + Send + Sync + 'static>(&mut self, value: T) -> Option<T> {
|
||||
let mut ext = self.extensions_mut();
|
||||
let old_data = ext.remove::<Data<T>>().map(|data| data.into_inner());
|
||||
ext.insert(Data::new(value));
|
||||
old_data
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn client_ip(&self) -> Option<IpAddr> {
|
||||
self.connection_info().remote().and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn read_body_bytes(&mut self) -> Result<Vec<u8>, Error> {
|
||||
let bytes =
|
||||
<Bytes as FromRequest<DefaultError>>::from_request(&self.0, &mut self.1).await?;
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Err: ErrorRenderer> From<WebRequest<Err>> for NtexExtractor<HttpRequest> {
|
||||
#[inline]
|
||||
fn from(request: WebRequest<Err>) -> Self {
|
||||
let (request, payload) = request.into_parts();
|
||||
Self(request, payload)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Err: ErrorRenderer> TryFrom<NtexExtractor<HttpRequest>> for WebRequest<Err> {
|
||||
type Error = NtexRejection;
|
||||
|
||||
#[inline]
|
||||
fn try_from(extractor: NtexExtractor<HttpRequest>) -> Result<Self, Self::Error> {
|
||||
Self::from_parts(extractor.0, extractor.1).map_err(|_| {
|
||||
let error = Error::new("fail to re-constructed `WebRequest`");
|
||||
Rejection::internal_server_error(error).into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpRequest> for NtexExtractor<HttpRequest> {
|
||||
#[inline]
|
||||
fn from(request: HttpRequest) -> Self {
|
||||
Self(request, Payload::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NtexExtractor<HttpRequest>> for HttpRequest {
|
||||
#[inline]
|
||||
fn from(extractor: NtexExtractor<HttpRequest>) -> Self {
|
||||
extractor.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest<DefaultError> for NtexExtractor<HttpRequest> {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
async fn from_request(req: &HttpRequest, payload: &mut Payload) -> Result<Self, Self::Error> {
|
||||
Ok(NtexExtractor(req.clone(), payload.take()))
|
||||
}
|
||||
}
|
|
@ -3,5 +3,7 @@ cfg_if::cfg_if! {
|
|||
pub(crate) mod actix_response;
|
||||
} else if #[cfg(feature = "axum")] {
|
||||
pub(crate) mod axum_response;
|
||||
} else if #[cfg(feature = "ntex")] {
|
||||
pub(crate) mod ntex_response;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
use ntex::{
|
||||
http::{
|
||||
body::Body,
|
||||
header::{self, HeaderName, HeaderValue},
|
||||
ResponseError, StatusCode,
|
||||
},
|
||||
web::{HttpRequest, HttpResponse, Responder, WebResponseError},
|
||||
};
|
||||
use std::fmt;
|
||||
use zino_core::{
|
||||
response::{Rejection, Response, ResponseCode},
|
||||
trace::TimingMetric,
|
||||
};
|
||||
|
||||
/// An HTTP response for `ntex`.
|
||||
pub struct NtexResponse<S: ResponseCode = StatusCode>(Response<S>);
|
||||
|
||||
impl<S: ResponseCode> From<Response<S>> for NtexResponse<S> {
|
||||
#[inline]
|
||||
fn from(response: Response<S>) -> Self {
|
||||
Self(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: ResponseCode> Responder for NtexResponse<S> {
|
||||
async fn respond_to(self, req: &HttpRequest) -> HttpResponse {
|
||||
let mut response = self.0;
|
||||
if !response.has_context() {
|
||||
let req = crate::Request::from(req.clone());
|
||||
response = response.context(&req);
|
||||
}
|
||||
|
||||
let mut res = build_http_response(&mut response);
|
||||
for (key, value) in response.finalize() {
|
||||
if let Ok(header_name) = HeaderName::try_from(key.as_ref()) {
|
||||
if let Ok(header_value) = HeaderValue::try_from(value) {
|
||||
res.headers_mut().insert(header_name, header_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
/// An HTTP rejection response for `ntex`.
|
||||
pub struct NtexRejection(Response<StatusCode>);
|
||||
|
||||
impl fmt::Debug for NtexRejection {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0.message().unwrap_or("OK"))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for NtexRejection {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.0.status_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rejection> for NtexRejection {
|
||||
#[inline]
|
||||
fn from(rejection: Rejection) -> Self {
|
||||
Self(Response::from(rejection))
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for NtexRejection {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
let mut response = self.0.clone();
|
||||
let mut res = build_http_response(&mut response);
|
||||
let request_id = response.request_id();
|
||||
if !request_id.is_nil() {
|
||||
if let Ok(header_value) = HeaderValue::try_from(request_id.to_string()) {
|
||||
let header_name = HeaderName::from_static("x-request-id");
|
||||
res.headers_mut().insert(header_name, header_value);
|
||||
}
|
||||
}
|
||||
|
||||
let (traceparent, tracestate) = response.trace_context();
|
||||
if let Ok(header_value) = HeaderValue::try_from(traceparent) {
|
||||
let header_name = HeaderName::from_static("traceparent");
|
||||
res.headers_mut().insert(header_name, header_value);
|
||||
}
|
||||
if let Ok(header_value) = HeaderValue::try_from(tracestate) {
|
||||
let header_name = HeaderName::from_static("tracestate");
|
||||
res.headers_mut().insert(header_name, header_value);
|
||||
}
|
||||
|
||||
let response_time = response.response_time();
|
||||
let timing = TimingMetric::new("total".into(), None, response_time.into());
|
||||
if let Ok(header_value) = HeaderValue::try_from(timing.to_string()) {
|
||||
let header_name = HeaderName::from_static("server-timing");
|
||||
res.headers_mut().insert(header_name, header_value);
|
||||
}
|
||||
|
||||
for (key, value) in response.headers() {
|
||||
if let Ok(header_name) = HeaderName::try_from(key.as_ref()) {
|
||||
if let Ok(header_value) = HeaderValue::try_from(value) {
|
||||
res.headers_mut().insert(header_name, header_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl WebResponseError for NtexRejection {
|
||||
#[inline]
|
||||
fn error_response(&self, _: &HttpRequest) -> HttpResponse {
|
||||
ResponseError::error_response(&self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build http response from `zino_core::response::Response`.
|
||||
fn build_http_response<S: ResponseCode>(response: &mut Response<S>) -> HttpResponse {
|
||||
match response.read_bytes() {
|
||||
Ok(data) => {
|
||||
let status_code = response
|
||||
.status_code()
|
||||
.try_into()
|
||||
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
let body = Body::from(data.to_vec());
|
||||
let mut res = HttpResponse::with_body(status_code, body);
|
||||
if let Ok(header_value) = HeaderValue::try_from(response.content_type()) {
|
||||
res.headers_mut().insert(header::CONTENT_TYPE, header_value);
|
||||
}
|
||||
res
|
||||
}
|
||||
Err(err) => {
|
||||
let status_code = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
let body = Body::from(err.to_string());
|
||||
let mut res = HttpResponse::with_body(status_code, body);
|
||||
res.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/plain; charset=utf-8"),
|
||||
);
|
||||
res
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue