Skip to content

Commit

Permalink
v0.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
MXWXZ committed Aug 28, 2024
1 parent b53182f commit 8bcdd28
Show file tree
Hide file tree
Showing 23 changed files with 961 additions and 314 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
# 0.3.0
## Breaking changes
1. `Logger::init` no longer consumes builder.
2. `GlobalState::logger` is now optional.
3. `LoggerBuilder::start` will return a guard to consume all logs when dropped.
4. `Router` now uses `Checker` trait to configure the checker.
5. `MemoryDB` is now wrapped with `Arc`.
6. `SessionStore` is now object safe.
7. `Extension` is now wrapped with `Arc`.

## New
1. utils: `load_rustls_config`.
2. Feature: `seaorm`, `csrf`.
3. `MemoryDB` now supports `keys` and `dels`.
4. `request::Middleware` now supports `trace_header` for trace id.
5. `Extension::lang` can be identified through the custom callback.
6. `Session` now support override `session_key`.

## Fix
1. Logger can consume all incoming logs when exited.

# 0.2.0
## Breaking changes
1. `Error` is using `anyhow::Error` as the backend.
Expand Down
94 changes: 77 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Actix Cloud is highly configurable. You can only enable needed features, impleme
- [response](#response) (Default: Disable)
- response-json
- [traceid](#traceid) (Default: Disable)
- [seaorm](#seaorm) (Default: Disable)
- [csrf](#csrf) (Default: Disable)

## Guide

Expand Down Expand Up @@ -115,27 +117,35 @@ RedisBackend::new("redis://user:[email protected]:6379/0").await.unwrap(),
```

### auth
Authentication is quite simple, you only need to implement an extractor and a checker.
Authentication is quite simple, you only need to implement a checker.

Extractor is used to extract your own authentication type from request. For example, assume we use 0 for guest and 1 for admin. Our authentication type is just `Vec<u32>`:
Checker is used to check the permission, the server will return 403 if the return value is false:
```
fn perm_extractor(req: &mut ServiceRequest) -> Vec<u32> {
let mut ret = Vec::new();
ret.push(0); // guest permission is assigned by default.
struct AuthChecker {
need_admin: bool,
}
// test if query string has `admin=1`.
let qs = QString::from(req.query_string());
if qs.get("admin").is_some_and(|x| x == "1") {
ret.push(1);
impl AuthChecker {
fn new(need_admin: bool) -> Self {
Self { need_admin }
}
ret
}
```
Checker is used to check the permission, the server will return 403 if the return value is false:
```
fn is_guest(p: Vec<u32>) -> bool {
p.into_iter().find(|x| *x == 0).is_some()
#[async_trait(?Send)]
impl Checker for AuthChecker {
async fn check(&self, req: &mut ServiceRequest) -> Result<bool> {
let qs = QString::from(req.query_string());
let is_admin = if qs.get("admin").is_some_and(|x| x == "1") {
true
} else {
false
};
if (is_admin && self.need_admin) || !self.need_admin {
Ok(true)
} else {
Ok(false)
}
}
}
```

Expand All @@ -149,6 +159,9 @@ Most features and usages are based on [actix-session](https://crates.io/crates/a
- MemoryDB is the only supported storage.
- Error uses `actix-cloud::error::Error`.
- You can set `_ttl` in the session to override the TTL of the session.
- You can set `_id` in the session for reverse search.
- Quote(") will be trimmed.
- Another key will be set in memorydb: `{_id}_{session_key}`. You can use `keys` function to find all session key binding to a specific id.

```
app.wrap(SessionMiddleware::builder(memorydb.clone(), Key::generate()).build())
Expand All @@ -167,7 +180,7 @@ Provide per-request extension.

Built-in middleware:
- Store in [extensions](https://docs.rs/actix-web/latest/actix_web/struct.HttpRequest.html#method.extensions_mut).
- If `i18n` feature is enabled, language is identified through the `lang` query parameter, or `locale.default` in `GlobalState`.
- If `i18n` feature is enabled, language is identified through the callback, or `locale.default` in `GlobalState`.

Enable built-in middleware:
```
Expand All @@ -178,7 +191,11 @@ Usage:
```
async fn handler(req: HttpRequest) -> impl Responder {
let ext = req.extensions();
let ext = ext.get::<actix_cloud::request::Extension>().unwrap();
let ext = ext.get::<Arc<actix_cloud::request::Extension>>().unwrap();
...
}
async fn handler(ext: ReqData<Arc<actix_cloud::request::Extension>>) -> impl Responder {
...
}
```
Expand Down Expand Up @@ -206,5 +223,48 @@ app.wrap(request::Middleware::new())

If you enable `request` feature, make sure it is before `TracingLogger` since the `trace_id` field is based on it.

### seaorm
Provide useful macros for [seaorm](https://crates.io/crates/sea-orm).

```
#[derive(...)]
#[sea_orm(...)]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub created_at: i64,
pub updated_at: i64,
}
#[entity_id(Uuid::new_v4())] // generate new for `id` field.
#[entity_timestamp] // automatically handle `created_at` and `updated_at` field.
impl ActiveModel {}
#[entity_behavior] // enable `entity_id` and `entity_timestamp`.
impl ActiveModelBehavior for ActiveModel {}
```

### csrf
We use [double submit](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#alternative-using-a-double-submit-cookie-pattern) to protect against CSRF attacks.

You can use `memorydb` to store and check CSRF tokens.

By default, CSRF checker is applied to:
- All [unsafe](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP) methods unless `CSRFType` is `Disabled`.
- All methods if `CSRFType` is `ForceHeader` or `ForceParam`.

Generally, `Param` and `ForceParam` type should only be used for websocket.

```
build_router(
route,
csrf::Middleware::new(
String::from("CSRF_TOKEN"), // csrf cookie
String::from("X-CSRF-Token"), // csrf header/param
|req, token| Box::pin(async { Ok(true) }) // csrf checker
),
);
```

## License
This project is licensed under the [MIT license](LICENSE).
3 changes: 2 additions & 1 deletion actix-cloud-codegen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "actix-cloud-codegen"
version = "0.1.1"
version = "0.2.0"
edition = "2021"
authors = ["MXWXZ <[email protected]>"]
description = "Proc macros for Actix Cloud."
Expand All @@ -10,6 +10,7 @@ repository = "https://github.com/MXWXZ/actix-cloud"
[features]
default = []
i18n = ["dep:rust-i18n-support"]
seaorm = []

[dependencies]
quote = "1.0.36"
Expand Down
150 changes: 150 additions & 0 deletions actix-cloud-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,153 @@ pub fn i18n(input: proc_macro::TokenStream) -> proc_macro::TokenStream {

code.into()
}

#[cfg(feature = "seaorm")]
/// Default timestamp generator.
///
/// Automatically generate `created_at` and `updated_at` on create and update.
///
/// Crate `chrono` will be used.
///
/// # Examples
/// ```ignore
/// pub struct Model {
/// ...
/// pub created_at: i64,
/// pub updated_at: i64,
/// }
///
/// #[entity_timestamp]
/// impl ActiveModel {}
/// ```
#[proc_macro_attribute]
pub fn entity_timestamp(_: TokenStream, input: TokenStream) -> TokenStream {
let mut entity = syn::parse_macro_input!(input as syn::ItemImpl);
entity.items.push(syn::parse_quote!(
fn entity_timestamp(&self, e: &mut Self, insert: bool) {
let tm: sea_orm::ActiveValue<i64> =
sea_orm::ActiveValue::set(chrono::Utc::now().timestamp_millis());
if insert {
e.created_at = tm.clone();
e.updated_at = tm.clone();
} else {
e.updated_at = tm.clone();
}
}
));
quote! {
#entity
}
.into()
}

#[cfg(feature = "seaorm")]
/// Default id generator.
///
/// Automatically generate `id` on create.
///
/// # Examples
/// ```ignore
/// pub struct Model {
/// id: i64,
/// ...
/// }
///
/// #[entity_id(rand_i64())]
/// impl ActiveModel {}
/// ```
#[proc_macro_attribute]
pub fn entity_id(attr: TokenStream, input: TokenStream) -> TokenStream {
let attr = syn::parse_macro_input!(attr as syn::ExprCall);
let mut entity = syn::parse_macro_input!(input as syn::ItemImpl);
entity.items.push(syn::parse_quote!(
fn entity_id(&self, e: &mut Self, insert: bool) {
if insert && e.id.is_not_set() {
e.id = sea_orm::ActiveValue::set(#attr);
}
}
));
quote! {
#entity
}
.into()
}

#[cfg(feature = "seaorm")]
/// Default entity behavior:
/// - `entity_id`
/// - `entity_timestamp`
///
/// # Examples
/// ```ignore
/// #[entity_id(rand_i64())]
/// #[entity_timestamp]
/// impl ActiveModel {}
///
/// #[entity_behavior]
/// impl ActiveModelBehavior for ActiveModel {}
/// ```
#[proc_macro_attribute]
pub fn entity_behavior(_: TokenStream, input: TokenStream) -> TokenStream {
let mut entity = syn::parse_macro_input!(input as syn::ItemImpl);

entity.items.push(syn::parse_quote!(
async fn before_save<C>(self, _: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
let mut new = self.clone();
self.entity_id(&mut new, insert);
self.entity_timestamp(&mut new, insert);
Ok(new)
}
));
quote! {
#[async_trait::async_trait]
#entity
}
.into()
}

#[cfg(feature = "seaorm")]
/// Implement `into` for entity to partial entity.
/// The fields should be exactly the same.
///
/// # Examples
/// ```ignore
/// #[partial_entity(users::Model)]
/// #[derive(Serialize)]
/// struct Rsp {
/// pub id: i64,
/// }
///
/// let y = users::Model {
/// id: ...,
/// name: ...,
/// ...
/// };
/// let x: Rsp = y.into();
/// ```
#[proc_macro_attribute]
pub fn partial_entity(attr: TokenStream, input: TokenStream) -> TokenStream {
let attr = syn::parse_macro_input!(attr as syn::ExprPath);
let input = syn::parse_macro_input!(input as syn::ItemStruct);
let name = &input.ident;
let mut fields = Vec::new();
for i in &input.fields {
let field_name = &i.ident;
fields.push(quote!(#field_name: self.#field_name,));
}

quote! {
#input
impl Into<#name> for #attr {
fn into(self) -> #name {
#name {
#(#fields)*
}
}
}
}
.into()
}
Loading

0 comments on commit 8bcdd28

Please sign in to comment.