Skip to content

Commit

Permalink
feat(server): authenticate with cookies and token (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoBorai authored Sep 10, 2024
1 parent 4422108 commit 35cfb07
Show file tree
Hide file tree
Showing 39 changed files with 907 additions and 306 deletions.
286 changes: 116 additions & 170 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ resolver = "2"

[workspace.dependencies]
anyhow = "1.0.86"
http-auth-basic = "0.3.3"
async-graphql = { version = "7.0.5", features = ["chrono", "decimal", "tracing"] }
async-graphql-axum = "7.0.5"
async-trait = "0.1.80"
axum = "0.7.5"
axum-extra = "0.9.3"
base64 = "0.22.1"
chrono = { version = "0.4.38", default-features = false }
cookie = "0.18.1"
dotenv = "0.15.0"
fake = "2.9.2"
graphql_client = "0.14.0"
Expand All @@ -29,12 +32,13 @@ lazy_static = "1.4.0"
leptos = "0.6"
leptos_meta = "0.6"
leptos_router = "0.6"
pxid = { version = "0.5", features = ["async-graphql"] }
leptos-use = "0.10"
pxid = { version = "1", features = ["async-graphql", "serde"] }
rand = "0.8.5"
regex = "1.9.3"
reqwest = "0.11"
rust-argon2 = "2.1.0"
rust-s3 = { version = "0.33.0", features = ["tokio-rustls-tls", "fail-on-err"], default-features = false }
rust-s3 = { version = "0.34.0", features = ["tokio-rustls-tls", "fail-on-err"], default-features = false }
sea-orm = "0.12"
sea-orm-cli = { version = "0.12", default-features = false }
sea-orm-migration = "0.12"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,14 @@ The client and server solution is available in this repository.

| Directory | Description |
| ------------------ | ------------------------------------------------------------------------------------------ |
| `crates/` | Contains GraphQL Server Logic, CLI and Domain libraries. Rust is the predominant language. |
| `crates/cli` | CLI used to manage the Server instance. run database migrations and other developer tasks |
| `crates/client` | HTTP Client used as bridge library between client interface and application services |
| `crates/core` | Domain Logic, includes Models, Value Objects, Repositories and Services |
| `crates/entity` | Entities generated from database |
| `crates/migration` | Database migrations |
| `crates/server` | HTTP Server Logic, uses Axum and GraphQL |
| `crates/test` | E2E Tests for the GraphQL Server |
| `crates/types` | Plain types shared between different application layers |
| `crates/web` | Web UI, written in Leptos |

## Testing
Expand Down
2 changes: 2 additions & 0 deletions crates/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ name = "townhall_client"
path = "src/lib.rs"

[dependencies]
anyhow = { workspace = true }
chrono = { workspace = true, features = ["js-sys", "wasmbind"] }
graphql_client = { workspace = true, features = ["reqwest"] }
http-auth-basic = { workspace = true }
pxid = { workspace = true, features = ["serde"] }
reqwest = { workspace = true, features = ["blocking", "json"] }
serde = { workspace = true }
Expand Down
26 changes: 16 additions & 10 deletions crates/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@ mod modules;

pub use modules::*;

pub(crate) const GRAPHQL_PATH: &str = "/graphql";

use std::fmt::Display;

use anyhow::{anyhow, Result};
use reqwest::Url;

use auth::AuthClient;

pub struct Client {
pub auth: AuthClient,
}

impl Default for Client {
fn default() -> Self {
Self::new()
}
}

impl Client {
pub fn new() -> Self {
Self {
auth: AuthClient::new(),
}
pub fn new<T: Clone + Display + TryInto<Url>>(domain: T) -> Result<Self> {
let domain = domain
.clone()
.try_into()
.map_err(|_| anyhow!("Provided domain \"{domain}\" is not a valid Url."))?;

Ok(Self {
auth: AuthClient::new(domain.clone()),
})
}
}
21 changes: 21 additions & 0 deletions crates/client/src/modules/auth/me/Me.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
query Me {
me {
user {
id
name
surname
username
email
createdAt
updatedAt
avatar {
id
url
}
}
error {
code
message
}
}
}
37 changes: 37 additions & 0 deletions crates/client/src/modules/auth/me/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use anyhow::{anyhow, bail, Result};
use graphql_client::reqwest::post_graphql;
use graphql_client::GraphQLQuery;
use pxid::Pxid;

use me::{MeMeError, MeMeUser, Variables};

use crate::{DateTime, GRAPHQL_PATH};

use super::AuthClient;

#[derive(GraphQLQuery)]
#[graphql(
response_derives = "Clone,Debug,Deserialize",
schema_path = "schema.json",
query_path = "src/modules/auth/me/Me.gql"
)]
pub struct Me {
pub user: Option<MeMeUser>,
pub error: Option<MeMeError>,
}

pub async fn me(auth_client: &AuthClient) -> Result<Me> {
let url = auth_client.domain.join(GRAPHQL_PATH)?;
let res = post_graphql::<Me, _>(&auth_client.client, url, Variables {})
.await
.map_err(|err| anyhow!("Failed to create token. {err}"))?;

if let Some(ref data) = res.data {
return Ok(Me {
user: data.me.user.to_owned(),
error: data.me.error.to_owned(),
});
}

bail!("Failed to get user data. err = {:#?}", res.errors.unwrap())
}
48 changes: 36 additions & 12 deletions crates/client/src/modules/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,57 @@
pub mod me;
pub mod token_create;
pub mod user_register;

use reqwest::Client;
use anyhow::{anyhow, Result};
use http_auth_basic::Credentials;
use reqwest::header::{HeaderValue, AUTHORIZATION};
use reqwest::{Client, Url};

pub struct AuthClient {
client: Client,
}

impl Default for AuthClient {
fn default() -> Self {
Self::new()
}
domain: Url,
}

impl AuthClient {
pub fn new() -> Self {
pub fn new(domain: Url) -> Self {
Self {
domain,
client: Client::new(),
}
}

pub async fn token_create(&self, email: String, password: String) -> token_create::TokenCreate {
token_create::token_create(&self.client, email, password).await
pub async fn login(&self, email: String, password: String) -> Result<()> {
let credentials = Credentials::new(email.as_str(), password.as_str());
let authorization = credentials.as_http_header();
let authorization = HeaderValue::from_str(authorization.as_str()).unwrap();
let url = self.domain.join("/api/v1/auth/login")?;

self.client
.get(url)
.header(AUTHORIZATION, authorization)
.send()
.await
.map_err(|err| anyhow!("Failed to authenticate. {err}"))?;

Ok(())
}

pub async fn token_create(
&self,
email: String,
password: String,
) -> Result<token_create::TokenCreate> {
token_create::token_create(self, email, password).await
}

pub async fn user_register(
&self,
input: user_register::UserRegisterInput,
) -> user_register::UserRegister {
user_register::user_register(&self.client, input).await
) -> Result<user_register::UserRegister> {
user_register::user_register(self, input).await
}

pub async fn me(&self) -> Result<me::Me> {
me::me(self).await
}
}
23 changes: 16 additions & 7 deletions crates/client/src/modules/auth/token_create/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use anyhow::{anyhow, Result};
use graphql_client::reqwest::post_graphql;
use graphql_client::GraphQLQuery;
use reqwest::Client;

use token_create::{TokenCreateTokenCreateError, TokenCreateTokenCreateToken};

use crate::GRAPHQL_PATH;

use super::AuthClient;

#[derive(GraphQLQuery)]
#[graphql(
response_derives = "Debug",
response_derives = "Clone,Debug",
schema_path = "schema.json",
query_path = "src/modules/auth/token_create/TokenCreate.gql"
)]
Expand All @@ -15,15 +19,20 @@ pub struct TokenCreate {
pub error: Option<TokenCreateTokenCreateError>,
}

pub async fn token_create(client: &Client, email: String, password: String) -> TokenCreate {
pub async fn token_create(
auth_client: &AuthClient,
email: String,
password: String,
) -> Result<TokenCreate> {
let variables = token_create::Variables { email, password };
let res = post_graphql::<TokenCreate, _>(client, "http://127.0.0.1:7878/graphql", variables)
let url = auth_client.domain.join(GRAPHQL_PATH)?;
let res = post_graphql::<TokenCreate, _>(&auth_client.client, url, variables)
.await
.unwrap();
.map_err(|err| anyhow!("Failed to create token. {err}"))?;
let data = res.data.unwrap().token_create;

TokenCreate {
Ok(TokenCreate {
token: data.token,
error: data.error,
}
})
}
22 changes: 14 additions & 8 deletions crates/client/src/modules/auth/user_register/mod.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
use anyhow::{anyhow, Result};
use graphql_client::reqwest::post_graphql;
use graphql_client::GraphQLQuery;
use pxid::Pxid;
use reqwest::Client;

use townhall_types::user::Email;
use user_register::{UserRegisterUserRegisterError, UserRegisterUserRegisterUser};

pub use crate::auth::user_register::user_register::UserRegisterInput;

use crate::DateTime;
use crate::{DateTime, GRAPHQL_PATH};

use super::AuthClient;

#[derive(GraphQLQuery)]
#[graphql(
response_derives = "Debug",
response_derives = "Clone,Debug",
schema_path = "schema.json",
query_path = "src/modules/auth/user_register/UserRegister.gql"
)]
Expand All @@ -21,15 +23,19 @@ pub struct UserRegister {
pub error: Option<UserRegisterUserRegisterError>,
}

pub async fn user_register(client: &Client, input: UserRegisterInput) -> UserRegister {
pub async fn user_register(
auth_client: &AuthClient,
input: UserRegisterInput,
) -> Result<UserRegister> {
let variables = user_register::Variables { input };
let res = post_graphql::<UserRegister, _>(client, "http://127.0.0.1:7878/graphql", variables)
let url = auth_client.domain.join(GRAPHQL_PATH)?;
let res = post_graphql::<UserRegister, _>(&auth_client.client, url, variables)
.await
.unwrap();
.map_err(|err| anyhow!("Failed to register user. {err}"))?;
let data = res.data.unwrap().user_register;

UserRegister {
Ok(UserRegister {
user: data.user,
error: data.error,
}
})
}
2 changes: 1 addition & 1 deletion crates/core/src/user/repository/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ impl UserRepository {
}

pub async fn update(&self, id: Pxid, dto: UpdateUserDto) -> Result<UserRecord> {
let user = entity::prelude::User::find_by_id(&id.to_string())
let user = entity::prelude::User::find_by_id(id.to_string())
.one(&*self.db)
.await
.map_err(|_| UserError::DatabaseError)?;
Expand Down
3 changes: 1 addition & 2 deletions crates/core/src/user/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,8 @@ impl<P: ImageProvider> UserService<P> {
pub async fn update_avatar(&self, id: Pxid, dto: UploadAvatarDto) -> Result<User> {
self.image_service
.validate(&dto.bytes, UseCase::Avatar)
.map_err(|err| {
.inspect_err(|err| {
tracing::warn!("Provided image is not valid for uploading as user's avatar");
err
})?;

let Some(user) = self.find_by_id(id).await? else {
Expand Down
4 changes: 4 additions & 0 deletions crates/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ async-graphql = { workspace = true }
async-graphql-axum = { workspace = true }
async-trait = { workspace = true }
axum = { workspace = true }
axum-extra = { workspace = true, features = ["cookie"] }
base64 = { workspace = true }
chrono = "0.4.38"
cookie = { workspace = true }
dotenv = { workspace = true }
http-auth-basic = { workspace = true }
jsonwebtoken = { workspace = true }
pxid = { workspace = true, features = ["async-graphql", "serde"] }
rand = { workspace = true }
rust-argon2 = { workspace = true }
rust-s3 = { workspace = true, features = ["tokio-rustls-tls", "fail-on-err"], default-features = false }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] }
tower-http = { workspace = true, features = ["cors"] }
Expand Down
2 changes: 2 additions & 0 deletions crates/server/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::env;

pub const ACCESS_TOKEN_COOKIE_NAME: &str = "access-token";

pub struct Config {
pub database_url: String,
pub jwt_secret: String,
Expand Down
Loading

0 comments on commit 35cfb07

Please sign in to comment.