mirror of
https://github.com/actix/examples
synced 2025-06-26 17:17:42 +02:00
restructure folders
This commit is contained in:
10
auth/casbin/Cargo.toml
Normal file
10
auth/casbin/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "actix-casbin-example"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.0.0-beta.21"
|
||||
casbin = "2.0.9"
|
||||
loge = {version = "0.4", default-features = false, features = ["colored", "chrono"]}
|
||||
tokio = { version = "1.16.1", features = ["sync"] }
|
26
auth/casbin/README.md
Normal file
26
auth/casbin/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Casbin
|
||||
|
||||
Basic integration of [Casbin-RS](https://github.com/casbin/casbin-rs) with [RBAC](https://en.wikipedia.org/wiki/Role-based_access_control) for Actix Web.
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
cd security/casbin
|
||||
```
|
||||
|
||||
Modify the files in the `rbac` directory and the code in the `src` directory as required.
|
||||
|
||||
## Running Server
|
||||
|
||||
```sh
|
||||
cd security/casbin
|
||||
cargo run (or ``cargo watch -x run``)
|
||||
|
||||
# Started http server: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
In this example, you can get the successful result at `http://localhost:8080/success` (accessible) and the failed result at `http://localhost:8080/fail` (inaccessible, `ERR_EMPTY_RESPONSE`).
|
||||
|
||||
## Others
|
||||
|
||||
- For more related examples of [Casbin-RS](https://github.com/casbin/casbin-rs): <https://github.com/casbin-rs/examples>
|
14
auth/casbin/rbac/rbac_model.conf
Normal file
14
auth/casbin/rbac/rbac_model.conf
Normal file
@ -0,0 +1,14 @@
|
||||
[request_definition]
|
||||
r = sub, obj, act
|
||||
|
||||
[policy_definition]
|
||||
p = sub, obj, act
|
||||
|
||||
[role_definition]
|
||||
g = _, _
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow))
|
||||
|
||||
[matchers]
|
||||
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
|
5
auth/casbin/rbac/rbac_policy.csv
Normal file
5
auth/casbin/rbac/rbac_policy.csv
Normal file
@ -0,0 +1,5 @@
|
||||
p, alice, data1, read
|
||||
p, bob, data2, write
|
||||
p, data2_admin, data2, read
|
||||
p, data2_admin, data2, write
|
||||
g, alice, data2_admin
|
|
55
auth/casbin/src/main.rs
Normal file
55
auth/casbin/src/main.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use casbin::{CoreApi, DefaultModel, Enforcer, FileAdapter, RbacApi};
|
||||
use std::io;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use actix_web::{middleware, web, App, HttpRequest, HttpResponse, HttpServer};
|
||||
|
||||
/// simple handle
|
||||
async fn success(
|
||||
enforcer: web::Data<RwLock<Enforcer>>,
|
||||
req: HttpRequest,
|
||||
) -> HttpResponse {
|
||||
let mut e = enforcer.write().await;
|
||||
println!("{:?}", req);
|
||||
assert_eq!(vec!["data2_admin"], e.get_roles_for_user("alice", None));
|
||||
|
||||
HttpResponse::Ok().body("Success: alice is data2_admin.")
|
||||
}
|
||||
|
||||
async fn fail(enforcer: web::Data<RwLock<Enforcer>>, req: HttpRequest) -> HttpResponse {
|
||||
let mut e = enforcer.write().await;
|
||||
println!("{:?}", req);
|
||||
assert_eq!(vec!["data1_admin"], e.get_roles_for_user("alice", None));
|
||||
|
||||
HttpResponse::Ok().body("Fail: alice is not data1_admin.") // In fact, it can't be displayed.
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
std::env::set_var("LOGE_FORMAT", "target");
|
||||
|
||||
loge::init();
|
||||
|
||||
let model = DefaultModel::from_file("rbac/rbac_model.conf")
|
||||
.await
|
||||
.unwrap();
|
||||
let adapter = FileAdapter::new("rbac/rbac_policy.csv");
|
||||
|
||||
let e = Enforcer::new(model, adapter).await.unwrap();
|
||||
let e = web::Data::new(RwLock::new(e)); // wrap enforcer into actix-state
|
||||
|
||||
//move is necessary to give closure below ownership of counter
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(e.clone()) // <- create app with shared state
|
||||
// enable logger
|
||||
.wrap(middleware::Logger::default())
|
||||
// register simple handler, handle all methods
|
||||
.service(web::resource("/success").to(success))
|
||||
.service(web::resource("/fail").to(fail))
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
10
auth/cookie-auth/Cargo.toml
Normal file
10
auth/cookie-auth/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "cookie-auth"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.0.0-beta.21"
|
||||
actix-identity = "0.4.0-beta.8"
|
||||
env_logger = "0.9.0"
|
||||
rand = "0.8.4"
|
32
auth/cookie-auth/README.md
Normal file
32
auth/cookie-auth/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
# cookie-auth
|
||||
|
||||
```sh
|
||||
cd session/cookie-auth
|
||||
cargo run
|
||||
# Starting http server: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
Testing with cookie auth with [curl](https://curl.haxx.se).
|
||||
|
||||
Login:
|
||||
|
||||
curl -v -b "auth-example=user1" -X POST http://localhost:8080/login
|
||||
< HTTP/1.1 302 Found
|
||||
< set-cookie: auth-example=GRm2Vku0UpFbJ3CNTKbndzIYHVGi8wc8eoXm/Axtf2BO; HttpOnly; Path=/
|
||||
< location: /
|
||||
|
||||
Uses a POST request with a Useridentity `user1`. A cookie is set and a redirect to home `/` follows.
|
||||
|
||||
Get:
|
||||
|
||||
Now with the cookie `auth-example` sent in a GET request, the `user1` is recognized.
|
||||
|
||||
curl -v -b "auth-example=GRm2Vku0UpFbJ3CNTKbndzIYHVGi8wc8eoXm/Axtf2BO" http://localhost:8080/
|
||||
* Connected to localhost (127.0.0.1) port 8080 (#0)
|
||||
> GET / HTTP/1.1
|
||||
> Host: localhost:8080
|
||||
> Cookie: auth-example=GRm2Vku0UpFbJ3CNTKbndzIYHVGi8wc8eoXm/Axtf2BO
|
||||
>
|
||||
< HTTP/1.1 200 OK
|
||||
<
|
||||
Hello user1
|
52
auth/cookie-auth/src/main.rs
Normal file
52
auth/cookie-auth/src/main.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use actix_identity::Identity;
|
||||
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
||||
use actix_web::{middleware, web, App, HttpResponse, HttpServer};
|
||||
use rand::Rng;
|
||||
|
||||
async fn index(id: Identity) -> String {
|
||||
format!(
|
||||
"Hello {}",
|
||||
id.identity().unwrap_or_else(|| "Anonymous".to_owned())
|
||||
)
|
||||
}
|
||||
|
||||
async fn login(id: Identity) -> HttpResponse {
|
||||
id.remember("user1".to_owned());
|
||||
HttpResponse::Found()
|
||||
.insert_header(("location", "/"))
|
||||
.finish()
|
||||
}
|
||||
|
||||
async fn logout(id: Identity) -> HttpResponse {
|
||||
id.forget();
|
||||
HttpResponse::Found()
|
||||
.insert_header(("location", "/"))
|
||||
.finish()
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
env_logger::init();
|
||||
|
||||
// Generate a random 32 byte key. Note that it is important to use a unique
|
||||
// private key for every project. Anyone with access to the key can generate
|
||||
// authentication cookies for any user!
|
||||
let private_key = rand::thread_rng().gen::<[u8; 32]>();
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(IdentityService::new(
|
||||
CookieIdentityPolicy::new(&private_key)
|
||||
.name("auth-example")
|
||||
.secure(false),
|
||||
))
|
||||
// enable logger - always register Actix Web Logger middleware last
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(web::resource("/login").route(web::post().to(login)))
|
||||
.service(web::resource("/logout").to(logout))
|
||||
.service(web::resource("/").route(web::get().to(index)))
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
10
auth/cookie-session/Cargo.toml
Normal file
10
auth/cookie-session/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "cookie-session"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.0.0-beta.21"
|
||||
actix-session = "0.5.0-beta.7"
|
||||
log = "0.4"
|
||||
env_logger = "0.9.0"
|
7
auth/cookie-session/README.md
Normal file
7
auth/cookie-session/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
## Cookie session example
|
||||
|
||||
```sh
|
||||
cd session/cookie-session
|
||||
cargo run
|
||||
# Starting http server: 127.0.0.1:8080
|
||||
```
|
45
auth/cookie-session/src/main.rs
Normal file
45
auth/cookie-session/src/main.rs
Normal file
@ -0,0 +1,45 @@
|
||||
//! Example of cookie based session
|
||||
//! Session data is stored in cookie, it is limited to 4kb
|
||||
//!
|
||||
//! [Redis session example](https://github.com/actix/examples/tree/master/redis-session)
|
||||
//!
|
||||
//! [User guide](https://actix.rs/docs/middleware/#user-sessions)
|
||||
|
||||
use actix_session::{CookieSession, Session};
|
||||
use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Result};
|
||||
|
||||
/// simple index handler with session
|
||||
async fn index(session: Session, req: HttpRequest) -> Result<&'static str> {
|
||||
log::info!("{:?}", req);
|
||||
|
||||
// RequestSession trait is used for session access
|
||||
let mut counter = 1;
|
||||
if let Some(count) = session.get::<i32>("counter")? {
|
||||
log::info!("SESSION value: {}", count);
|
||||
counter = count + 1;
|
||||
session.insert("counter", counter)?;
|
||||
} else {
|
||||
session.insert("counter", counter)?;
|
||||
}
|
||||
|
||||
Ok("welcome!")
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
env_logger::init();
|
||||
log::info!("Starting http server: 127.0.0.1:8080");
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
// enable logger
|
||||
.wrap(Logger::default())
|
||||
// cookie session middleware
|
||||
.wrap(CookieSession::signed(&[0; 32]).secure(false))
|
||||
.service(web::resource("/").to(index))
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
16
auth/redis-session/Cargo.toml
Normal file
16
auth/redis-session/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "redis_session"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.0.0-beta.21"
|
||||
actix-session = "0.5.0-beta.7"
|
||||
actix-redis = "0.10.0-beta.5"
|
||||
env_logger = "0.9.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-test = "0.1.0-beta.11"
|
||||
time = "0.3.7"
|
14
auth/redis-session/README.md
Normal file
14
auth/redis-session/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# redis-session
|
||||
|
||||
```sh
|
||||
cd session/redis-sessions
|
||||
cargo run
|
||||
# Starting http server: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
## Available Routes
|
||||
|
||||
- [GET /](http://localhost:8080/)
|
||||
- [POST /do_something](http://localhost:8080/do_something)
|
||||
- [POST /login](http://localhost:8080/login)
|
||||
- [POST /logout](http://localhost:8080/logout)
|
277
auth/redis-session/src/main.rs
Normal file
277
auth/redis-session/src/main.rs
Normal file
@ -0,0 +1,277 @@
|
||||
//! Example of login and logout using redis-based sessions
|
||||
//!
|
||||
//! Every request gets a session, corresponding to a cache entry and cookie.
|
||||
//! At login, the session key changes and session state in cache re-assigns.
|
||||
//! At logout, session state in cache is removed and cookie is invalidated.
|
||||
//!
|
||||
use actix_redis::RedisSession;
|
||||
use actix_session::Session;
|
||||
use actix_web::{
|
||||
middleware, web,
|
||||
web::{get, post, resource},
|
||||
App, HttpResponse, HttpServer, Result,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct IndexResponse {
|
||||
user_id: Option<String>,
|
||||
counter: i32,
|
||||
}
|
||||
|
||||
async fn index(session: Session) -> Result<HttpResponse> {
|
||||
let user_id: Option<String> = session.get::<String>("user_id").unwrap();
|
||||
let counter: i32 = session
|
||||
.get::<i32>("counter")
|
||||
.unwrap_or(Some(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(HttpResponse::Ok().json(IndexResponse { user_id, counter }))
|
||||
}
|
||||
|
||||
async fn do_something(session: Session) -> Result<HttpResponse> {
|
||||
let user_id: Option<String> = session.get::<String>("user_id").unwrap();
|
||||
let counter: i32 = session
|
||||
.get::<i32>("counter")
|
||||
.unwrap_or(Some(0))
|
||||
.map_or(1, |inner| inner + 1);
|
||||
session.insert("counter", counter)?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(IndexResponse { user_id, counter }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Identity {
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
async fn login(user_id: web::Json<Identity>, session: Session) -> Result<HttpResponse> {
|
||||
let id = user_id.into_inner().user_id;
|
||||
session.insert("user_id", &id)?;
|
||||
session.renew();
|
||||
|
||||
let counter: i32 = session
|
||||
.get::<i32>("counter")
|
||||
.unwrap_or(Some(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(HttpResponse::Ok().json(IndexResponse {
|
||||
user_id: Some(id),
|
||||
counter,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn logout(session: Session) -> Result<String> {
|
||||
let id: Option<String> = session.get("user_id")?;
|
||||
if let Some(x) = id {
|
||||
session.purge();
|
||||
Ok(format!("Logged out: {}", x))
|
||||
} else {
|
||||
Ok("Could not log out anonymous user".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
std::env::set_var("RUST_LOG", "actix_web=info,actix_redis=info");
|
||||
env_logger::init();
|
||||
|
||||
// Generate a random 32 byte key. Note that it is important to use a unique
|
||||
// private key for every project. Anyone with access to the key can generate
|
||||
// authentication cookies for any user!
|
||||
let private_key = actix_web::cookie::Key::generate();
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
// redis session middleware
|
||||
.wrap(RedisSession::new("127.0.0.1:6379", private_key.master()))
|
||||
// enable logger - always register Actix Web Logger middleware last
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(resource("/").route(get().to(index)))
|
||||
.service(resource("/do_something").route(post().to(do_something)))
|
||||
.service(resource("/login").route(post().to(login)))
|
||||
.service(resource("/logout").route(post().to(logout)))
|
||||
})
|
||||
.bind(("127.0.0.1", 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use actix_web::{
|
||||
middleware,
|
||||
web::{get, post, resource},
|
||||
App,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_workflow() {
|
||||
let private_key = actix_web::cookie::Key::generate();
|
||||
let srv = actix_test::start(move || {
|
||||
App::new()
|
||||
.wrap(
|
||||
RedisSession::new("127.0.0.1:6379", private_key.master())
|
||||
.cookie_name("test-session"),
|
||||
)
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(resource("/").route(get().to(index)))
|
||||
.service(resource("/do_something").route(post().to(do_something)))
|
||||
.service(resource("/login").route(post().to(login)))
|
||||
.service(resource("/logout").route(post().to(logout)))
|
||||
});
|
||||
|
||||
// Step 1: GET index
|
||||
// - set-cookie actix-session should NOT be in response (session data is empty)
|
||||
// - response should be: {"counter": 0, "user_id": None}
|
||||
let request = srv.get("/").send();
|
||||
let mut resp_1 = request.await.unwrap();
|
||||
assert!(resp_1.cookies().unwrap().is_empty());
|
||||
let result_1 = resp_1.json::<IndexResponse>().await.unwrap();
|
||||
assert_eq!(
|
||||
result_1,
|
||||
IndexResponse {
|
||||
user_id: None,
|
||||
counter: 0
|
||||
}
|
||||
);
|
||||
|
||||
// Step 2: POST to do_something, including session cookie #1 in request
|
||||
// - adds new session state in redis: {"counter": 1}
|
||||
// - response should be: {"counter": 1, "user_id": None}
|
||||
let req_3 = srv.post("/do_something").send();
|
||||
let mut resp_3 = req_3.await.unwrap();
|
||||
let cookie_1 = resp_3
|
||||
.cookies()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.find(|c| c.name() == "test-session")
|
||||
.unwrap();
|
||||
let result_3 = resp_3.json::<IndexResponse>().await.unwrap();
|
||||
assert_eq!(
|
||||
result_3,
|
||||
IndexResponse {
|
||||
user_id: None,
|
||||
counter: 1
|
||||
}
|
||||
);
|
||||
|
||||
// Step 3: POST again to do_something, including session cookie #1 in request
|
||||
// - updates session state in redis: {"counter": 2}
|
||||
// - response should be: {"counter": 2, "user_id": None}
|
||||
let req_4 = srv.post("/do_something").cookie(cookie_1.clone()).send();
|
||||
let mut resp_4 = req_4.await.unwrap();
|
||||
let result_4 = resp_4.json::<IndexResponse>().await.unwrap();
|
||||
assert_eq!(
|
||||
result_4,
|
||||
IndexResponse {
|
||||
user_id: None,
|
||||
counter: 2
|
||||
}
|
||||
);
|
||||
|
||||
// Step 4: POST to login, including session cookie #1 in request
|
||||
// - set-cookie actix-session will be in response (session cookie #2)
|
||||
// - updates session state in redis: {"counter": 2, "user_id": "ferris"}
|
||||
let req_5 = srv
|
||||
.post("/login")
|
||||
.cookie(cookie_1.clone())
|
||||
.send_json(&json!({"user_id": "ferris"}));
|
||||
let mut resp_5 = req_5.await.unwrap();
|
||||
let cookie_2 = resp_5
|
||||
.cookies()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.find(|c| c.name() == "test-session")
|
||||
.unwrap();
|
||||
assert_ne!(cookie_1.value(), cookie_2.value());
|
||||
|
||||
let result_5 = resp_5.json::<IndexResponse>().await.unwrap();
|
||||
assert_eq!(
|
||||
result_5,
|
||||
IndexResponse {
|
||||
user_id: Some("ferris".into()),
|
||||
counter: 2
|
||||
}
|
||||
);
|
||||
|
||||
// Step 5: GET index, including session cookie #2 in request
|
||||
// - response should be: {"counter": 2, "user_id": "ferris"}
|
||||
let req_6 = srv.get("/").cookie(cookie_2.clone()).send();
|
||||
let mut resp_6 = req_6.await.unwrap();
|
||||
let result_6 = resp_6.json::<IndexResponse>().await.unwrap();
|
||||
assert_eq!(
|
||||
result_6,
|
||||
IndexResponse {
|
||||
user_id: Some("ferris".into()),
|
||||
counter: 2
|
||||
}
|
||||
);
|
||||
|
||||
// Step 6: POST again to do_something, including session cookie #2 in request
|
||||
// - updates session state in redis: {"counter": 3, "user_id": "ferris"}
|
||||
// - response should be: {"counter": 2, "user_id": None}
|
||||
let req_7 = srv.post("/do_something").cookie(cookie_2.clone()).send();
|
||||
let mut resp_7 = req_7.await.unwrap();
|
||||
let result_7 = resp_7.json::<IndexResponse>().await.unwrap();
|
||||
assert_eq!(
|
||||
result_7,
|
||||
IndexResponse {
|
||||
user_id: Some("ferris".into()),
|
||||
counter: 3
|
||||
}
|
||||
);
|
||||
|
||||
// Step 7: GET index, including session cookie #1 in request
|
||||
// - set-cookie actix-session will be in response (session cookie #3)
|
||||
// - response should be: {"counter": 0, "user_id": None}
|
||||
let req_8 = srv.get("/").cookie(cookie_1.clone()).send();
|
||||
let mut resp_8 = req_8.await.unwrap();
|
||||
assert!(resp_8.cookies().unwrap().is_empty());
|
||||
let result_8 = resp_8.json::<IndexResponse>().await.unwrap();
|
||||
assert_eq!(
|
||||
result_8,
|
||||
IndexResponse {
|
||||
user_id: None,
|
||||
counter: 0
|
||||
}
|
||||
);
|
||||
|
||||
// Step 8: POST to logout, including session cookie #2
|
||||
// - set-cookie actix-session will be in response with session cookie #2
|
||||
// invalidation logic
|
||||
let req_9 = srv.post("/logout").cookie(cookie_2.clone()).send();
|
||||
let resp_9 = req_9.await.unwrap();
|
||||
let cookie_4 = resp_9
|
||||
.cookies()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.into_iter()
|
||||
.find(|c| c.name() == "test-session")
|
||||
.unwrap();
|
||||
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
assert_ne!(
|
||||
now.year(),
|
||||
cookie_4.expires().unwrap().datetime().unwrap().year()
|
||||
);
|
||||
|
||||
// Step 9: GET index, including session cookie #2 in request
|
||||
// - set-cookie actix-session will be in response (session cookie #3)
|
||||
// - response should be: {"counter": 0, "user_id": None}
|
||||
let req_10 = srv.get("/").cookie(cookie_2.clone()).send();
|
||||
let mut resp_10 = req_10.await.unwrap();
|
||||
let result_10 = resp_10.json::<IndexResponse>().await.unwrap();
|
||||
assert_eq!(
|
||||
result_10,
|
||||
IndexResponse {
|
||||
user_id: None,
|
||||
counter: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
23
auth/simple-auth-server/Cargo.toml
Normal file
23
auth/simple-auth-server/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "simple-auth-server"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.0.0-beta.21"
|
||||
actix-identity = "0.4.0-beta.8"
|
||||
|
||||
chrono = { version = "0.4.6", features = ["serde"] }
|
||||
derive_more = "0.99.0"
|
||||
diesel = { version = "1.4.5", features = ["postgres", "uuidv07", "r2d2", "chrono"] }
|
||||
dotenv = "0.15"
|
||||
env_logger = "0.9.0"
|
||||
futures = "0.3.1"
|
||||
r2d2 = "0.8"
|
||||
rust-argon2 = "1.0.0"
|
||||
lazy_static = "1.4.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sparkpost = "0.5.2"
|
||||
uuid = { version = "0.8.2", features = ["serde", "v4"] }
|
||||
time = "0.3.7"
|
47
auth/simple-auth-server/README.md
Normal file
47
auth/simple-auth-server/README.md
Normal file
@ -0,0 +1,47 @@
|
||||
## Auth Web Microservice with Rust using Actix Web
|
||||
|
||||
### Flow of the event would look like this:
|
||||
|
||||
- Registers with email address ➡ Receive an 📨 with a link to verify
|
||||
- Follow the link ➡ register with same email and a password
|
||||
- Login with email and password ➡ Get verified and receive auth cookie
|
||||
|
||||
### Available Routes
|
||||
|
||||
- [GET /](http://localhost:8080/)
|
||||
- [POST /api/invitation](http://localhost:8080/api/invitation)
|
||||
- [POST /api/invitation/:invitation_id](http://localhost:8080/api/invitation/:invitation_id)
|
||||
- [GET /api/auth](http://localhost:8080/api/auth)
|
||||
- [POST /api/auth](http://localhost:8080/api/auth)
|
||||
- [DELETE /api/auth](http://localhost:8080/api/auth)
|
||||
|
||||
### Crates Used
|
||||
|
||||
- [actix-web](https://crates.io/crates/actix-web) // Actix Web is a simple, pragmatic and extremely fast web framework for Rust.
|
||||
- [rust-argon2](https://crates.io/crates/rust-argon2) // crate for hashing passwords using the cryptographically-secure Argon2 hashing algorithm.
|
||||
- [chrono](https://crates.io/crates/chrono) // Date and time library for Rust.
|
||||
- [diesel](https://crates.io/crates/diesel) // A safe, extensible ORM and Query Builder for PostgreSQL, SQLite, and MySQL.
|
||||
- [dotenv](https://crates.io/crates/dotenv) // A dotenv implementation for Rust.
|
||||
- [derive_more](https://crates.io/crates/derive_more) // Convenience macros to derive tarits easily
|
||||
- [env_logger](https://crates.io/crates/env_logger) // A logging implementation for log which is configured via an environment variable.
|
||||
- [futures](https://crates.io/crates/futures) // An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces.
|
||||
- [lazy_static](https://docs.rs/lazy_static) // A macro for declaring lazily evaluated statics.
|
||||
- [r2d2](https://crates.io/crates/r2d2) // A generic connection pool.
|
||||
- [serde](https://crates.io/crates/serde) // A generic serialization/deserialization framework.
|
||||
- [serde_json](https://crates.io/crates/serde_json) // A JSON serialization file format.
|
||||
- [serde_derive](https://crates.io/crates/serde_derive) // Macros 1.1 implementation of #[derive(Serialize, Deserialize)].
|
||||
- [sparkpost](https://crates.io/crates/sparkpost) // Rust bindings for sparkpost email api v1.
|
||||
- [uuid](https://crates.io/crates/uuid) // A library to generate and parse UUIDs.
|
||||
|
||||
|
||||
Read the full tutorial series on [gill.net.in](https://gill.net.in)
|
||||
|
||||
- [Auth Web Microservice with Rust using Actix Web v2 - Complete Tutorial](https://gill.net.in/posts/auth-microservice-rust-actix-web1.0-diesel-complete-tutorial/)
|
||||
|
||||
### Dependencies
|
||||
|
||||
On Ubuntu 19.10:
|
||||
|
||||
```
|
||||
sudo apt install libclang-dev libpq-dev
|
||||
```
|
5
auth/simple-auth-server/diesel.toml
Normal file
5
auth/simple-auth-server/diesel.toml
Normal file
@ -0,0 +1,5 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
0
auth/simple-auth-server/migrations/.gitkeep
Normal file
0
auth/simple-auth-server/migrations/.gitkeep
Normal file
@ -0,0 +1,6 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
@ -0,0 +1,36 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
|
||||
|
||||
|
||||
-- Sets up a trigger for the given table to automatically set a column called
|
||||
-- `updated_at` whenever the row is modified (unless `updated_at` was included
|
||||
-- in the modified columns)
|
||||
--
|
||||
-- # Example
|
||||
--
|
||||
-- ```sql
|
||||
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
|
||||
--
|
||||
-- SELECT diesel_manage_updated_at('users');
|
||||
-- ```
|
||||
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF (
|
||||
NEW IS DISTINCT FROM OLD AND
|
||||
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||
) THEN
|
||||
NEW.updated_at := current_timestamp;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE users;
|
@ -0,0 +1,6 @@
|
||||
-- Your SQL goes here
|
||||
CREATE TABLE users (
|
||||
email VARCHAR(100) NOT NULL UNIQUE PRIMARY KEY,
|
||||
hash VARCHAR(122) NOT NULL, --argon hash
|
||||
created_at TIMESTAMP NOT NULL
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE invitations;
|
@ -0,0 +1,6 @@
|
||||
-- Your SQL goes here
|
||||
CREATE TABLE invitations (
|
||||
id UUID NOT NULL UNIQUE PRIMARY KEY,
|
||||
email VARCHAR(100) NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL
|
||||
);
|
74
auth/simple-auth-server/src/auth_handler.rs
Normal file
74
auth/simple-auth-server/src/auth_handler.rs
Normal file
@ -0,0 +1,74 @@
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{dev::Payload, web, Error, FromRequest, HttpRequest, HttpResponse};
|
||||
use diesel::prelude::*;
|
||||
use diesel::PgConnection;
|
||||
use futures::future::{err, ok, Ready};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::errors::ServiceError;
|
||||
use crate::models::{Pool, SlimUser, User};
|
||||
use crate::utils::verify;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AuthData {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
// we need the same data
|
||||
// simple aliasing makes the intentions clear and its more readable
|
||||
pub type LoggedUser = SlimUser;
|
||||
|
||||
impl FromRequest for LoggedUser {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<LoggedUser, Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
|
||||
if let Ok(identity) = Identity::from_request(req, pl).into_inner() {
|
||||
if let Some(user_json) = identity.identity() {
|
||||
if let Ok(user) = serde_json::from_str(&user_json) {
|
||||
return ok(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
err(ServiceError::Unauthorized.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn logout(id: Identity) -> HttpResponse {
|
||||
id.forget();
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
auth_data: web::Json<AuthData>,
|
||||
id: Identity,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let user = web::block(move || query(auth_data.into_inner(), pool)).await??;
|
||||
|
||||
let user_string = serde_json::to_string(&user).unwrap();
|
||||
id.remember(user_string);
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
pub async fn get_me(logged_user: LoggedUser) -> HttpResponse {
|
||||
HttpResponse::Ok().json(logged_user)
|
||||
}
|
||||
/// Diesel query
|
||||
fn query(auth_data: AuthData, pool: web::Data<Pool>) -> Result<SlimUser, ServiceError> {
|
||||
use crate::schema::users::dsl::{email, users};
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
let mut items = users
|
||||
.filter(email.eq(&auth_data.email))
|
||||
.load::<User>(conn)?;
|
||||
|
||||
if let Some(user) = items.pop() {
|
||||
if let Ok(matching) = verify(&user.hash, &auth_data.password) {
|
||||
if matching {
|
||||
return Ok(user.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ServiceError::Unauthorized)
|
||||
}
|
67
auth/simple-auth-server/src/email_service.rs
Normal file
67
auth/simple-auth-server/src/email_service.rs
Normal file
@ -0,0 +1,67 @@
|
||||
// email_service.rs
|
||||
use crate::errors::ServiceError;
|
||||
use crate::models::Invitation;
|
||||
use sparkpost::transmission::{
|
||||
EmailAddress, Message, Options, Recipient, Transmission, TransmissionResponse,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref API_KEY: String = std::env::var("SPARKPOST_API_KEY").expect("SPARKPOST_API_KEY must be set");
|
||||
}
|
||||
|
||||
pub fn send_invitation(invitation: &Invitation) -> Result<(), ServiceError> {
|
||||
let tm = Transmission::new_eu(API_KEY.as_str());
|
||||
let sending_email = std::env::var("SENDING_EMAIL_ADDRESS")
|
||||
.expect("SENDING_EMAIL_ADDRESS must be set");
|
||||
// new email message with sender name and email
|
||||
let mut email = Message::new(EmailAddress::new(sending_email, "Let's Organise"));
|
||||
|
||||
let options = Options {
|
||||
open_tracking: false,
|
||||
click_tracking: false,
|
||||
transactional: true,
|
||||
sandbox: false,
|
||||
inline_css: false,
|
||||
start_time: None,
|
||||
};
|
||||
|
||||
// recipient from the invitation email
|
||||
let recipient: Recipient = invitation.email.as_str().into();
|
||||
|
||||
let email_body = format!(
|
||||
"Please click on the link below to complete registration. <br/>
|
||||
<a href=\"http://localhost:3000/register.html?id={}&email={}\">
|
||||
http://localhost:3030/register</a> <br>
|
||||
your Invitation expires on <strong>{}</strong>",
|
||||
invitation.id,
|
||||
invitation.email,
|
||||
invitation.expires_at.format("%I:%M %p %A, %-d %B, %C%y")
|
||||
);
|
||||
|
||||
// complete the email message with details
|
||||
email
|
||||
.add_recipient(recipient)
|
||||
.options(options)
|
||||
.subject("You have been invited to join Simple-Auth-Server Rust")
|
||||
.html(email_body);
|
||||
|
||||
let result = tm.send(&email);
|
||||
|
||||
// Note that we only print out the error response from email api
|
||||
match result {
|
||||
Ok(res) => match res {
|
||||
TransmissionResponse::ApiResponse(api_res) => {
|
||||
println!("API Response: \n {:#?}", api_res);
|
||||
Ok(())
|
||||
}
|
||||
TransmissionResponse::ApiError(errors) => {
|
||||
println!("Response Errors: \n {:#?}", &errors);
|
||||
Err(ServiceError::InternalServerError)
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
println!("Send Email Error: \n {:#?}", error);
|
||||
Err(ServiceError::InternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
59
auth/simple-auth-server/src/errors.rs
Normal file
59
auth/simple-auth-server/src/errors.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use actix_web::{error::ResponseError, HttpResponse};
|
||||
use derive_more::Display;
|
||||
use diesel::result::{DatabaseErrorKind, Error as DBError};
|
||||
use std::convert::From;
|
||||
use uuid::Error as ParseError;
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum ServiceError {
|
||||
#[display(fmt = "Internal Server Error")]
|
||||
InternalServerError,
|
||||
|
||||
#[display(fmt = "BadRequest: {}", _0)]
|
||||
BadRequest(String),
|
||||
|
||||
#[display(fmt = "Unauthorized")]
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
// impl ResponseError trait allows to convert our errors into http responses with appropriate data
|
||||
impl ResponseError for ServiceError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match self {
|
||||
ServiceError::InternalServerError => HttpResponse::InternalServerError()
|
||||
.json("Internal Server Error, Please try later"),
|
||||
ServiceError::BadRequest(ref message) => {
|
||||
HttpResponse::BadRequest().json(message)
|
||||
}
|
||||
ServiceError::Unauthorized => {
|
||||
HttpResponse::Unauthorized().json("Unauthorized")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we can return early in our handlers if UUID provided by the user is not valid
|
||||
// and provide a custom message
|
||||
impl From<ParseError> for ServiceError {
|
||||
fn from(_: ParseError) -> ServiceError {
|
||||
ServiceError::BadRequest("Invalid UUID".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DBError> for ServiceError {
|
||||
fn from(error: DBError) -> ServiceError {
|
||||
// Right now we just care about UniqueViolation from diesel
|
||||
// But this would be helpful to easily map errors as our app grows
|
||||
match error {
|
||||
DBError::DatabaseError(kind, info) => {
|
||||
if let DatabaseErrorKind::UniqueViolation = kind {
|
||||
let message =
|
||||
info.details().unwrap_or_else(|| info.message()).to_string();
|
||||
return ServiceError::BadRequest(message);
|
||||
}
|
||||
ServiceError::InternalServerError
|
||||
}
|
||||
_ => ServiceError::InternalServerError,
|
||||
}
|
||||
}
|
||||
}
|
47
auth/simple-auth-server/src/invitation_handler.rs
Normal file
47
auth/simple-auth-server/src/invitation_handler.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use actix_web::{web, HttpResponse};
|
||||
use diesel::{prelude::*, PgConnection};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::email_service::send_invitation;
|
||||
use crate::models::{Invitation, Pool};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct InvitationData {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
pub async fn post_invitation(
|
||||
invitation_data: web::Json<InvitationData>,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
// run diesel blocking code
|
||||
web::block(move || create_invitation(invitation_data.into_inner().email, pool))
|
||||
.await??;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
fn create_invitation(
|
||||
eml: String,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<(), crate::errors::ServiceError> {
|
||||
let invitation = dbg!(query(eml, pool)?);
|
||||
send_invitation(&invitation)
|
||||
}
|
||||
|
||||
/// Diesel query
|
||||
fn query(
|
||||
eml: String,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<Invitation, crate::errors::ServiceError> {
|
||||
use crate::schema::invitations::dsl::invitations;
|
||||
|
||||
let new_invitation: Invitation = eml.into();
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
|
||||
let inserted_invitation = diesel::insert_into(invitations)
|
||||
.values(&new_invitation)
|
||||
.get_result(conn)?;
|
||||
|
||||
Ok(inserted_invitation)
|
||||
}
|
74
auth/simple-auth-server/src/main.rs
Normal file
74
auth/simple-auth-server/src/main.rs
Normal file
@ -0,0 +1,74 @@
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
|
||||
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
||||
use actix_web::{middleware, web, App, HttpServer};
|
||||
use diesel::prelude::*;
|
||||
use diesel::r2d2::{self, ConnectionManager};
|
||||
use time::Duration;
|
||||
|
||||
mod auth_handler;
|
||||
mod email_service;
|
||||
mod errors;
|
||||
mod invitation_handler;
|
||||
mod models;
|
||||
mod register_handler;
|
||||
mod schema;
|
||||
mod utils;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
"simple-auth-server=debug,actix_web=info,actix_server=info",
|
||||
);
|
||||
env_logger::init();
|
||||
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
|
||||
// create db connection pool
|
||||
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||
let pool: models::Pool = r2d2::Pool::builder()
|
||||
.build(manager)
|
||||
.expect("Failed to create pool.");
|
||||
let domain: String =
|
||||
std::env::var("DOMAIN").unwrap_or_else(|_| "localhost".to_string());
|
||||
|
||||
// Start http server
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(web::Data::new(pool.clone()))
|
||||
// enable logger
|
||||
.wrap(middleware::Logger::default())
|
||||
.wrap(IdentityService::new(
|
||||
CookieIdentityPolicy::new(utils::SECRET_KEY.as_bytes())
|
||||
.name("auth")
|
||||
.path("/")
|
||||
.domain(domain.as_str())
|
||||
.max_age(Duration::days(1))
|
||||
.secure(false), // this can only be true if you have https
|
||||
))
|
||||
.app_data(web::JsonConfig::default().limit(4096))
|
||||
// everything under '/api/' route
|
||||
.service(
|
||||
web::scope("/api")
|
||||
.service(
|
||||
web::resource("/invitation")
|
||||
.route(web::post().to(invitation_handler::post_invitation)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/register/{invitation_id}")
|
||||
.route(web::post().to(register_handler::register_user)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/auth")
|
||||
.route(web::post().to(auth_handler::login))
|
||||
.route(web::delete().to(auth_handler::logout))
|
||||
.route(web::get().to(auth_handler::get_me)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.bind("127.0.0.1:3000")?
|
||||
.run()
|
||||
.await
|
||||
}
|
57
auth/simple-auth-server/src/models.rs
Normal file
57
auth/simple-auth-server/src/models.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use super::schema::*;
|
||||
use diesel::{r2d2::ConnectionManager, PgConnection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// type alias to use in multiple places
|
||||
pub type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
|
||||
#[table_name = "users"]
|
||||
pub struct User {
|
||||
pub email: String,
|
||||
pub hash: String,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn from_details<S: Into<String>, T: Into<String>>(email: S, pwd: T) -> Self {
|
||||
User {
|
||||
email: email.into(),
|
||||
hash: pwd.into(),
|
||||
created_at: chrono::Local::now().naive_local(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
|
||||
#[table_name = "invitations"]
|
||||
pub struct Invitation {
|
||||
pub id: uuid::Uuid,
|
||||
pub email: String,
|
||||
pub expires_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
// any type that implements Into<String> can be used to create Invitation
|
||||
impl<T> From<T> for Invitation
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
fn from(email: T) -> Self {
|
||||
Invitation {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
email: email.into(),
|
||||
expires_at: chrono::Local::now().naive_local() + chrono::Duration::hours(24),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SlimUser {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl From<User> for SlimUser {
|
||||
fn from(user: User) -> Self {
|
||||
SlimUser { email: user.email }
|
||||
}
|
||||
}
|
61
auth/simple-auth-server/src/register_handler.rs
Normal file
61
auth/simple-auth-server/src/register_handler.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use actix_web::{web, HttpResponse};
|
||||
use diesel::prelude::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::errors::ServiceError;
|
||||
use crate::models::{Invitation, Pool, SlimUser, User};
|
||||
use crate::utils::hash_password;
|
||||
// UserData is used to extract data from a post request by the client
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserData {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub async fn register_user(
|
||||
invitation_id: web::Path<String>,
|
||||
user_data: web::Json<UserData>,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let user = web::block(move || {
|
||||
query(
|
||||
invitation_id.into_inner(),
|
||||
user_data.into_inner().password,
|
||||
pool,
|
||||
)
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(HttpResponse::Ok().json(&user))
|
||||
}
|
||||
|
||||
fn query(
|
||||
invitation_id: String,
|
||||
password: String,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<SlimUser, crate::errors::ServiceError> {
|
||||
use crate::schema::invitations::dsl::{id, invitations};
|
||||
use crate::schema::users::dsl::users;
|
||||
let invitation_id = uuid::Uuid::parse_str(&invitation_id)?;
|
||||
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
invitations
|
||||
.filter(id.eq(invitation_id))
|
||||
.load::<Invitation>(conn)
|
||||
.map_err(|_db_error| ServiceError::BadRequest("Invalid Invitation".into()))
|
||||
.and_then(|mut result| {
|
||||
if let Some(invitation) = result.pop() {
|
||||
// if invitation is not expired
|
||||
if invitation.expires_at > chrono::Local::now().naive_local() {
|
||||
// try hashing the password, else return the error that will be converted to ServiceError
|
||||
let password: String = hash_password(&password)?;
|
||||
dbg!(&password);
|
||||
let user = User::from_details(invitation.email, password);
|
||||
let inserted_user: User =
|
||||
diesel::insert_into(users).values(&user).get_result(conn)?;
|
||||
dbg!(&inserted_user);
|
||||
return Ok(inserted_user.into());
|
||||
}
|
||||
}
|
||||
Err(ServiceError::BadRequest("Invalid Invitation".into()))
|
||||
})
|
||||
}
|
17
auth/simple-auth-server/src/schema.rs
Normal file
17
auth/simple-auth-server/src/schema.rs
Normal file
@ -0,0 +1,17 @@
|
||||
table! {
|
||||
invitations (id) {
|
||||
id -> Uuid,
|
||||
email -> Varchar,
|
||||
expires_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
users (email) {
|
||||
email -> Varchar,
|
||||
hash -> Varchar,
|
||||
created_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
allow_tables_to_appear_in_same_query!(invitations, users,);
|
28
auth/simple-auth-server/src/utils.rs
Normal file
28
auth/simple-auth-server/src/utils.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use crate::errors::ServiceError;
|
||||
use argon2::{self, Config};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref SECRET_KEY: String = std::env::var("SECRET_KEY").unwrap_or_else(|_| "0123".repeat(8));
|
||||
}
|
||||
|
||||
const SALT: &[u8] = b"supersecuresalt";
|
||||
|
||||
// WARNING THIS IS ONLY FOR DEMO PLEASE DO MORE RESEARCH FOR PRODUCTION USE
|
||||
pub fn hash_password(password: &str) -> Result<String, ServiceError> {
|
||||
let config = Config {
|
||||
secret: SECRET_KEY.as_bytes(),
|
||||
..Default::default()
|
||||
};
|
||||
argon2::hash_encoded(password.as_bytes(), SALT, &config).map_err(|err| {
|
||||
dbg!(err);
|
||||
ServiceError::InternalServerError
|
||||
})
|
||||
}
|
||||
|
||||
pub fn verify(hash: &str, password: &str) -> Result<bool, ServiceError> {
|
||||
argon2::verify_encoded_ext(hash, password.as_bytes(), SECRET_KEY.as_bytes(), &[])
|
||||
.map_err(|err| {
|
||||
dbg!(err);
|
||||
ServiceError::Unauthorized
|
||||
})
|
||||
}
|
31
auth/simple-auth-server/static/index.html
Normal file
31
auth/simple-auth-server/static/index.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Actix Web - Auth App</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
|
||||
<script src="main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login">
|
||||
<h1>Email Invitation</h1>
|
||||
|
||||
<p>Please enter your email receive Invitation</p>
|
||||
<input class="field" type="text" placeholder="email" id="email" /> <br />
|
||||
<input class="btn" type="submit" value="Send Email" onclick="sendVerificationEmail()" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<script>
|
||||
function sendVerificationEmail() {
|
||||
let email = document.querySelector('#email');
|
||||
|
||||
post('api/invitation', { email: email.value }).then(data => {
|
||||
alert('Please check your email.');
|
||||
email.value = '';
|
||||
console.error(data);
|
||||
});
|
||||
}
|
||||
</script>
|
37
auth/simple-auth-server/static/main.css
Normal file
37
auth/simple-auth-server/static/main.css
Normal file
@ -0,0 +1,37 @@
|
||||
/* CSSTerm.com Easy CSS login form */
|
||||
|
||||
.login {
|
||||
width:600px;
|
||||
margin:auto;
|
||||
border:1px #CCC solid;
|
||||
padding:0px 30px;
|
||||
background-color: #3b6caf;
|
||||
color:#FFF;
|
||||
}
|
||||
|
||||
.field {
|
||||
background: #1e4f8a;
|
||||
border:1px #03306b solid;
|
||||
padding:10px;
|
||||
margin:5px 25px;
|
||||
width:215px;
|
||||
color:#FFF;
|
||||
}
|
||||
|
||||
.login h1, p, .chbox, .btn {
|
||||
margin-left:25px;
|
||||
color:#fff;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #00CCFF;
|
||||
border:1px #03306b solid;
|
||||
padding:10px 30px;
|
||||
font-weight:bold;
|
||||
margin:25px 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.forgot {
|
||||
color:#fff;
|
||||
}
|
19
auth/simple-auth-server/static/main.js
Normal file
19
auth/simple-auth-server/static/main.js
Normal file
@ -0,0 +1,19 @@
|
||||
function post(url = ``, data = {}) {
|
||||
// Default options are marked with *
|
||||
return fetch(url, {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
mode: 'cors', // no-cors, cors, *same-origin
|
||||
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
redirect: 'follow', // manual, *follow, error
|
||||
referrer: 'no-referrer', // no-referrer, *client
|
||||
body: JSON.stringify(data), // body data type must match "Content-Type" header
|
||||
}).then(response => response.json()); // parses response to JSON
|
||||
}
|
||||
|
||||
// window.addEventListener('load', function() {
|
||||
// console.log('All assets are loaded');
|
||||
// console.log(getUrlVars());
|
||||
// });
|
44
auth/simple-auth-server/static/register.html
Normal file
44
auth/simple-auth-server/static/register.html
Normal file
@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Actix Web - Auth App</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
|
||||
<script src="main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login">
|
||||
<h1>Register Account</h1>
|
||||
|
||||
<p>Please enter your email and new password</p>
|
||||
<input class="field" type="text" placeholder="email" id="email" />
|
||||
<input class="field" type="password" placeholder="Password" id="password" />
|
||||
<input class="btn" type="submit" value="Register" onclick="register()" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<script>
|
||||
function getUrlVars() {
|
||||
var vars = {};
|
||||
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) {
|
||||
vars[key] = value;
|
||||
});
|
||||
return vars;
|
||||
}
|
||||
function register() {
|
||||
let password = document.querySelector('#password');
|
||||
let invitation_id = getUrlVars().id;
|
||||
|
||||
post('api/register/' + invitation_id, { password: password.value }).then(data => {
|
||||
password.value = '';
|
||||
console.error(data);
|
||||
});
|
||||
}
|
||||
window.addEventListener('load', function() {
|
||||
let email = document.querySelector('#email');
|
||||
email.value = getUrlVars().email;
|
||||
console.log(getUrlVars());
|
||||
});
|
||||
</script>
|
Reference in New Issue
Block a user