Created
October 2, 2021 14:02
-
-
Save Eraden/23e09f36430ba865f3180d3cc138ab48 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// shared/auth/src/lib.rs | |
// The MIT License (MIT) | |
// Copyright (c) 2021 Tsumanu | |
// Access token with Bearer | |
use std::future::Future; | |
use std::rc::Rc; | |
use actix_web::dev::{Payload, ServiceRequest, ServiceResponse}; | |
use actix_web::web::Data; | |
use actix_web::{Error, FromRequest, HttpRequest, Result}; | |
use data::{AccessToken, User}; | |
use db_actor::{DbExecutor, Handle}; | |
use futures_util::future::{ready, Ready}; | |
pub use self::middleware::IdentityService; | |
mod middleware; | |
#[macro_export] | |
macro_rules! require_user { | |
($id: ident) => {{ | |
match $id.user().await { | |
None => { | |
log::warn!("Terminating authorized access"); | |
return HttpResponse::Unauthorized().body(""); | |
} | |
Some(user) => user, | |
} | |
}}; | |
} | |
struct IdentityItem { | |
access_token: Option<data::AccessToken>, | |
database: Data<db_actor::DbExecutor>, | |
} | |
impl IdentityItem { | |
pub fn new( | |
access_token: Option<data::AccessToken>, | |
database: Data<db_actor::DbExecutor>, | |
) -> Self { | |
Self { | |
database, | |
access_token, | |
} | |
} | |
} | |
#[derive(Debug, Clone)] | |
enum UserState { | |
NoInitialized, | |
Anonymouse, | |
User(Rc<data::User>), | |
} | |
#[derive(Clone, Debug)] | |
pub struct Identity { | |
database: Data<db_actor::DbExecutor>, | |
access_token: Option<AccessToken>, | |
user: UserState, | |
} | |
impl Identity { | |
pub fn new(access_token: Option<AccessToken>, database: Data<DbExecutor>) -> Self { | |
Self { | |
database, | |
access_token, | |
user: UserState::NoInitialized, | |
} | |
} | |
pub fn access_token(&self) -> Option<&AccessToken> { | |
self.access_token.as_ref() | |
} | |
pub async fn user(&mut self) -> Option<Rc<User>> { | |
if let Some(u) = self.try_user_ref() { | |
return u; | |
} | |
let token = *self.access_token()?; | |
self.load_user(token).await; | |
self.user_ref() | |
} | |
fn try_user_ref(&self) -> Option<Option<Rc<User>>> { | |
match &self.user { | |
UserState::Anonymouse => Some(None), | |
UserState::User(user) => Some(Some(user.clone())), | |
_ => None, | |
} | |
} | |
fn user_ref(&self) -> Option<Rc<User>> { | |
match &self.user { | |
UserState::User(user) => Some(user.clone()), | |
_ => None, | |
} | |
} | |
async fn load_user(&mut self, token: AccessToken) { | |
self.user = self | |
.database | |
.handle(db_actor::AuthorizeUser { token }) | |
.await | |
.ok() | |
.map(Rc::new) | |
.map_or_else(|| UserState::Anonymouse, UserState::User); | |
log::info!("CURRENT USER IS {:?}", self.user); | |
} | |
} | |
impl FromRequest for Identity { | |
type Config = (); | |
type Error = Error; | |
type Future = Ready<Result<Identity, Error>>; | |
#[inline] | |
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { | |
let id = req | |
.extensions() | |
.get::<IdentityItem>() | |
.map(|id| Identity::new(id.access_token, id.database.clone())) | |
.expect("Identity service unavailable"); | |
ready(Ok(id)) | |
} | |
} | |
pub trait IdentityPolicy: Sized + 'static { | |
type Future: Future<Output = Result<Option<String>, Error>>; | |
type ResponseFuture: Future<Output = Result<(), Error>>; | |
fn from_request(&self, req: &mut ServiceRequest) -> Self::Future; | |
fn to_response<B>( | |
&self, | |
identity: Option<String>, | |
changed: bool, | |
response: &mut ServiceResponse<B>, | |
) -> Self::ResponseFuture; | |
} | |
pub trait RequestIdentity { | |
fn get_identity(&self) -> Option<String>; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// shared/data/lib.rs | |
// The MIT License (MIT) | |
// Copyright (c) 2021 Tsumanu | |
#[cfg_attr(feature = "db", derive(sqlx::Type))] | |
#[cfg_attr(feature = "db", sqlx(transparent))] | |
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] | |
pub struct UserId(pub i32); | |
#[cfg_attr(feature = "db", derive(sqlx::Type))] | |
#[cfg_attr(feature = "db", sqlx(transparent))] | |
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] | |
pub struct ShareToken(pub uuid::Uuid); | |
#[cfg_attr(feature = "db", derive(sqlx::Type))] | |
#[cfg_attr(feature = "db", sqlx(transparent))] | |
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] | |
pub struct AccecssToken(pub uuid::Uuid); | |
#[cfg_attr(feature = "db", derive(sqlx::Type))] | |
#[cfg_attr(feature = "db", sqlx(transparent))] | |
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] | |
pub struct Username(pub String); | |
#[cfg_attr(feature = "db", derive(sqlx::Type))] | |
#[cfg_attr(feature = "db", sqlx(transparent))] | |
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] | |
pub struct Email(String); | |
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] | |
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] | |
pub struct User { | |
pub id: UserId, | |
pub email: Email, | |
pub username: Username, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// actors/src/lib.rs | |
// The MIT License (MIT) | |
// Copyright (c) 2021 Tsumanu | |
pub type DbExecutor = sqlx::PgPool; | |
#[async_trait] | |
pub trait Handle<Msg> { | |
type Result; | |
async fn handle(&self, msg: Msg) -> Result<Self::Result>; | |
} | |
pub struct AuthorizeUser { | |
pub token: data::AccessToken, | |
} | |
#[async_trait] | |
impl Handle<AuthorizeUser> for sqlx::PgPool { | |
type Result = data::User; | |
async fn handle(&self, msg: AuthorizeUser) -> Result<Self::Result> { | |
let user = sqlx::query_as( | |
r#" | |
SELECT users.id, users.email, users.username | |
FROM users | |
INNER JOIN tokens ON users.id = tokens.user_id | |
WHERE tokens.access_token = $1 | |
"#, | |
) | |
.bind(msg.token) | |
.fetch_one(self) | |
.await?; | |
Ok(user) | |
} | |
} | |
pub struct AllUsers {} | |
#[async_trait] | |
impl Handle<AllUsers> for DbExecutor { | |
type Result = Vec<data::User>; | |
async fn handle(&self, _msg: AllUsers) -> Result<Self::Result> { | |
let users = sqlx::query_as( | |
r#" | |
SELECT id, email, username | |
FROM users | |
"#, | |
) | |
.fetch_all(self) | |
.await?; | |
Ok(users) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// shared/auth/src/middleware.rs | |
// The MIT License (MIT) | |
// Copyright (c) 2021 Tsumanu | |
use std::error::Error as StdError; | |
use std::rc::Rc; | |
use actix_web::body::{AnyBody, MessageBody}; | |
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; | |
use actix_web::web::Data; | |
use actix_web::{Error, HttpMessage, Result}; | |
use futures_util::future::{ready, FutureExt as _FE, LocalBoxFuture, Ready, TryFutureExt as TFE_}; | |
use super::*; | |
static AUTHORIZATION: &str = "Authorization"; | |
static BEARER: &str = "Bearer "; | |
static TOKEN_LEN: usize = 36; | |
#[derive(Default, Debug)] | |
pub struct IdentityService; | |
impl<S, B> Transform<S, ServiceRequest> for IdentityService | |
where | |
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, | |
S::Future: 'static, | |
B: MessageBody + 'static, | |
B::Error: StdError, | |
{ | |
type Response = ServiceResponse; | |
type Error = Error; | |
type Transform = IdentityServiceMiddleware<S>; | |
type InitError = (); | |
type Future = Ready<Result<Self::Transform, Self::InitError>>; | |
fn new_transform(&self, service: S) -> Self::Future { | |
ready(Ok(IdentityServiceMiddleware { | |
service: Rc::new(service), | |
})) | |
} | |
} | |
pub struct IdentityServiceMiddleware<S> { | |
pub(crate) service: Rc<S>, | |
} | |
impl<S> Clone for IdentityServiceMiddleware<S> { | |
fn clone(&self) -> Self { | |
Self { | |
service: Rc::clone(&self.service), | |
} | |
} | |
} | |
impl<S, B> Service<ServiceRequest> for IdentityServiceMiddleware<S> | |
where | |
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, | |
S::Future: 'static, | |
B: MessageBody + 'static, | |
B::Error: StdError, | |
{ | |
type Response = ServiceResponse; | |
type Error = Error; | |
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; | |
actix_service::forward_ready!(service); | |
fn call(&self, req: ServiceRequest) -> Self::Future { | |
let srv = Rc::clone(&self.service); | |
async move { | |
let database = req.app_data::<Data<db_actor::DbExecutor>>(); | |
let token = req | |
.headers() | |
.get(AUTHORIZATION) | |
.and_then(|s| s.to_str().ok()) | |
.filter(|s| { | |
log::info!("Bearer header {:?}", s); | |
s.contains(BEARER) | |
}) | |
.filter(|s| s.len() == BEARER.len() + TOKEN_LEN) | |
.and_then(|s| { | |
log::info!("Only token {:?}", &s[BEARER.len()..]); | |
uuid::Uuid::parse_str(&s[BEARER.len()..]).ok() | |
}) | |
.map(data::AccessToken::new); | |
log::info!("token {:?}", token); | |
req.extensions_mut() | |
.insert(IdentityItem::new(token, database.unwrap().clone())); | |
Ok(srv | |
.call(req) | |
.await? | |
.map_body(|_, body| AnyBody::from_message(body))) | |
} | |
.map_ok(|res| res.map_body(|_, body| AnyBody::from_message(body))) | |
.boxed_local() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// server/src/routes/users.rs | |
// The MIT License (MIT) | |
// Copyright (c) 2021 Tsumanu | |
use actix_web::web::Data; | |
use actix_web::{get, HttpResponse}; | |
use auth::{require_user, Identity}; | |
use data::*; | |
use db_actor::{self as db, DbExecutor, Handle}; | |
#[get("/api/v1/users")] | |
pub async fn users(database: Data<DbExecutor>, mut id: Identity) -> HttpResponse { | |
let _user = require_user!(id); | |
let users: Vec<User> = database | |
.handle(db::AllUsers {}) | |
.await | |
.ok() | |
.unwrap_or_default(); | |
let bytes = bincode::serialize(&users).unwrap_or_default(); | |
HttpResponse::Ok().body(bytes) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment