BLOG-85 Implement OIDC authentication #93
1057
backend/Cargo.lock
generated
1057
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["server", "feature/image", "feature/post"]
|
members = ["server", "feature/auth", "feature/image", "feature/post"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
@ -8,13 +8,19 @@ edition = "2024"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
actix-multipart = "0.7.2"
|
actix-multipart = "0.7.2"
|
||||||
|
actix-session = { version = "0.10.1", features = ["redis-session"] }
|
||||||
actix-web = "4.10.2"
|
actix-web = "4.10.2"
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
env_logger = "0.11.8"
|
env_logger = "0.11.8"
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
|
hex = "0.4.3"
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
|
openidconnect = { version = "4.0.1", features = [
|
||||||
|
"reqwest",
|
||||||
|
"reqwest-blocking",
|
||||||
|
] }
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
sqlx = { version = "0.8.5", features = [
|
sqlx = { version = "0.8.5", features = [
|
||||||
@ -26,5 +32,6 @@ sqlx = { version = "0.8.5", features = [
|
|||||||
tokio = { version = "1.45.0", features = ["full"] }
|
tokio = { version = "1.45.0", features = ["full"] }
|
||||||
|
|
||||||
server.path = "server"
|
server.path = "server"
|
||||||
|
auth.path = "feature/auth"
|
||||||
image.path = "feature/image"
|
image.path = "feature/image"
|
||||||
post.path = "feature/post"
|
post.path = "feature/post"
|
||||||
|
@ -21,4 +21,11 @@ ENV DATABASE_PORT=5432
|
|||||||
ENV DATABASE_USER=postgres
|
ENV DATABASE_USER=postgres
|
||||||
ENV DATABASE_PASSWORD=
|
ENV DATABASE_PASSWORD=
|
||||||
ENV DATABASE_NAME=postgres
|
ENV DATABASE_NAME=postgres
|
||||||
|
ENV REIDS_URL=redis://127.0.0.1:6379
|
||||||
|
ENV SESSION_KEY='64-bytes-hex-string-which-can-be-generated-by-`openssl rand -hex 64`'
|
||||||
|
ENV OIDC_ISSUER_URL=
|
||||||
|
ENV OIDC_REDIRECT_URL=
|
||||||
|
ENV OIDC_CLIENT_ID=
|
||||||
|
ENV OIDC_CLIENT_SECRET=
|
||||||
|
|
||||||
CMD ["./server"]
|
CMD ["./server"]
|
||||||
|
12
backend/feature/auth/Cargo.toml
Normal file
12
backend/feature/auth/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "auth"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-session.workspace = true
|
||||||
|
actix-web.workspace = true
|
||||||
|
async-trait.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
openidconnect.workspace = true
|
||||||
|
serde.workspace = true
|
2
backend/feature/auth/src/adapter.rs
Normal file
2
backend/feature/auth/src/adapter.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod delivery;
|
||||||
|
pub mod gateway;
|
3
backend/feature/auth/src/adapter/delivery.rs
Normal file
3
backend/feature/auth/src/adapter/delivery.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod auth_controller;
|
||||||
|
pub mod oidc_callback_query_dto;
|
||||||
|
pub mod user_response_dto;
|
66
backend/feature/auth/src/adapter/delivery/auth_controller.rs
Normal file
66
backend/feature/auth/src/adapter/delivery/auth_controller.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
adapter::delivery::{
|
||||||
|
oidc_callback_query_dto::OidcCallbackQueryDto, user_response_dto::UserResponseDto,
|
||||||
|
},
|
||||||
|
application::{
|
||||||
|
error::auth_error::AuthError,
|
||||||
|
use_case::{
|
||||||
|
exchange_auth_code_use_case::ExchangeAuthCodeUseCase,
|
||||||
|
get_auth_url_use_case::{AuthUrl, GetAuthUrlUseCase},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AuthController: Send + Sync {
|
||||||
|
fn oidc_login(&self) -> Result<AuthUrl, AuthError>;
|
||||||
|
|
||||||
|
async fn oidc_callback(
|
||||||
|
&self,
|
||||||
|
query: OidcCallbackQueryDto,
|
||||||
|
expected_state: &str,
|
||||||
|
expected_nonce: &str,
|
||||||
|
) -> Result<UserResponseDto, AuthError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthControllerImpl {
|
||||||
|
get_auth_url_use_case: Arc<dyn GetAuthUrlUseCase>,
|
||||||
|
exchange_auth_code_use_case: Arc<dyn ExchangeAuthCodeUseCase>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthControllerImpl {
|
||||||
|
pub fn new(
|
||||||
|
get_auth_url_use_case: Arc<dyn GetAuthUrlUseCase>,
|
||||||
|
exchange_auth_code_use_case: Arc<dyn ExchangeAuthCodeUseCase>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
get_auth_url_use_case,
|
||||||
|
exchange_auth_code_use_case,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AuthController for AuthControllerImpl {
|
||||||
|
fn oidc_login(&self) -> Result<AuthUrl, AuthError> {
|
||||||
|
self.get_auth_url_use_case.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn oidc_callback(
|
||||||
|
&self,
|
||||||
|
query: OidcCallbackQueryDto,
|
||||||
|
expected_state: &str,
|
||||||
|
expected_nonce: &str,
|
||||||
|
) -> Result<UserResponseDto, AuthError> {
|
||||||
|
let result = self
|
||||||
|
.exchange_auth_code_use_case
|
||||||
|
.execute(&query.code, &query.state, expected_state, expected_nonce)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
result.map(|user| UserResponseDto::from(user))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct OidcCallbackQueryDto {
|
||||||
|
pub code: String,
|
||||||
|
pub state: String,
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::domain::entity::user::User;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct UserResponseDto {
|
||||||
|
pub source_id: String,
|
||||||
|
pub displayed_name: String,
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<User> for UserResponseDto {
|
||||||
|
fn from(user: User) -> Self {
|
||||||
|
UserResponseDto {
|
||||||
|
source_id: user.source_id,
|
||||||
|
displayed_name: user.displayed_name,
|
||||||
|
email: user.email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
backend/feature/auth/src/adapter/gateway.rs
Normal file
3
backend/feature/auth/src/adapter/gateway.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod oidc_claims_response_dto;
|
||||||
|
pub mod auth_oidc_service;
|
||||||
|
pub mod auth_repository_impl;
|
@ -0,0 +1,16 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
adapter::gateway::oidc_claims_response_dto::OidcClaimsResponseDto,
|
||||||
|
application::{error::auth_error::AuthError, use_case::get_auth_url_use_case::AuthUrl},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AuthOidcService: Send + Sync {
|
||||||
|
fn get_auth_url(&self) -> Result<AuthUrl, AuthError>;
|
||||||
|
async fn exchange_auth_code(
|
||||||
|
&self,
|
||||||
|
code: &str,
|
||||||
|
expected_nonce: &str,
|
||||||
|
) -> Result<OidcClaimsResponseDto, AuthError>;
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
adapter::gateway::auth_oidc_service::AuthOidcService,
|
||||||
|
application::{
|
||||||
|
error::auth_error::AuthError, gateway::auth_repository::AuthRepository,
|
||||||
|
use_case::get_auth_url_use_case::AuthUrl,
|
||||||
|
},
|
||||||
|
domain::entity::user::User,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct AuthRepositoryImpl {
|
||||||
|
auth_oidc_service: Arc<dyn AuthOidcService>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthRepositoryImpl {
|
||||||
|
pub fn new(auth_oidc_service: Arc<dyn AuthOidcService>) -> Self {
|
||||||
|
Self { auth_oidc_service }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AuthRepository for AuthRepositoryImpl {
|
||||||
|
fn get_auth_url(&self) -> Result<AuthUrl, AuthError> {
|
||||||
|
self.auth_oidc_service.get_auth_url()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exchange_auth_code(
|
||||||
|
&self,
|
||||||
|
code: &str,
|
||||||
|
expected_nonce: &str,
|
||||||
|
) -> Result<User, AuthError> {
|
||||||
|
self.auth_oidc_service
|
||||||
|
.exchange_auth_code(code, expected_nonce)
|
||||||
|
.await
|
||||||
|
.map(|dto| dto.into_entity())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
use crate::domain::entity::user::User;
|
||||||
|
|
||||||
|
pub struct OidcClaimsResponseDto {
|
||||||
|
pub sub: String,
|
||||||
|
pub preferred_username: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OidcClaimsResponseDto {
|
||||||
|
pub fn into_entity(self) -> User {
|
||||||
|
User {
|
||||||
|
source_id: self.sub,
|
||||||
|
displayed_name: self.preferred_username.unwrap_or_default(),
|
||||||
|
email: self.email.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
backend/feature/auth/src/application.rs
Normal file
3
backend/feature/auth/src/application.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod error;
|
||||||
|
pub mod gateway;
|
||||||
|
pub mod use_case;
|
1
backend/feature/auth/src/application/error.rs
Normal file
1
backend/feature/auth/src/application/error.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod auth_error;
|
8
backend/feature/auth/src/application/error/auth_error.rs
Normal file
8
backend/feature/auth/src/application/error/auth_error.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum AuthError {
|
||||||
|
OidcError(String),
|
||||||
|
InvalidState,
|
||||||
|
InvalidNonce,
|
||||||
|
InvalidAuthCode,
|
||||||
|
InvalidIdToken,
|
||||||
|
}
|
1
backend/feature/auth/src/application/gateway.rs
Normal file
1
backend/feature/auth/src/application/gateway.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod auth_repository;
|
@ -0,0 +1,13 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
application::{error::auth_error::AuthError, use_case::get_auth_url_use_case::AuthUrl},
|
||||||
|
domain::entity::user::User,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AuthRepository: Send + Sync {
|
||||||
|
fn get_auth_url(&self) -> Result<AuthUrl, AuthError>;
|
||||||
|
async fn exchange_auth_code(&self, code: &str, expected_nonce: &str)
|
||||||
|
-> Result<User, AuthError>;
|
||||||
|
}
|
2
backend/feature/auth/src/application/use_case.rs
Normal file
2
backend/feature/auth/src/application/use_case.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod exchange_auth_code_use_case;
|
||||||
|
pub mod get_auth_url_use_case;
|
@ -0,0 +1,48 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
application::{error::auth_error::AuthError, gateway::auth_repository::AuthRepository},
|
||||||
|
domain::entity::user::User,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait ExchangeAuthCodeUseCase: Send + Sync {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
code: &str,
|
||||||
|
received_state: &str,
|
||||||
|
expected_state: &str,
|
||||||
|
expected_nonce: &str,
|
||||||
|
) -> Result<User, AuthError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExchangeAuthCodeUseCaseImpl {
|
||||||
|
auth_repository: Arc<dyn AuthRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExchangeAuthCodeUseCaseImpl {
|
||||||
|
pub fn new(auth_repository: Arc<dyn AuthRepository>) -> Self {
|
||||||
|
Self { auth_repository }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ExchangeAuthCodeUseCase for ExchangeAuthCodeUseCaseImpl {
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
code: &str,
|
||||||
|
received_state: &str,
|
||||||
|
expected_state: &str,
|
||||||
|
expected_nonce: &str,
|
||||||
|
) -> Result<User, AuthError> {
|
||||||
|
if received_state != expected_state {
|
||||||
|
return Err(AuthError::InvalidState);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.auth_repository
|
||||||
|
.exchange_auth_code(code, expected_nonce)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::application::{error::auth_error::AuthError, gateway::auth_repository::AuthRepository};
|
||||||
|
|
||||||
|
pub trait GetAuthUrlUseCase: Send + Sync {
|
||||||
|
fn execute(&self) -> Result<AuthUrl, AuthError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LoginUseCaseImpl {
|
||||||
|
auth_repository: Arc<dyn AuthRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginUseCaseImpl {
|
||||||
|
pub fn new(auth_repository: Arc<dyn AuthRepository>) -> Self {
|
||||||
|
Self { auth_repository }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetAuthUrlUseCase for LoginUseCaseImpl {
|
||||||
|
fn execute(&self) -> Result<AuthUrl, AuthError> {
|
||||||
|
self.auth_repository.get_auth_url()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthUrl {
|
||||||
|
pub url: String,
|
||||||
|
pub state: String,
|
||||||
|
pub nonce: String,
|
||||||
|
}
|
1
backend/feature/auth/src/domain.rs
Normal file
1
backend/feature/auth/src/domain.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod entity;
|
1
backend/feature/auth/src/domain/entity.rs
Normal file
1
backend/feature/auth/src/domain/entity.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod user;
|
5
backend/feature/auth/src/domain/entity/user.rs
Normal file
5
backend/feature/auth/src/domain/entity/user.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub struct User {
|
||||||
|
pub source_id: String,
|
||||||
|
pub displayed_name: String,
|
||||||
|
pub email: String,
|
||||||
|
}
|
2
backend/feature/auth/src/framework.rs
Normal file
2
backend/feature/auth/src/framework.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod oidc;
|
||||||
|
pub mod web;
|
1
backend/feature/auth/src/framework/oidc.rs
Normal file
1
backend/feature/auth/src/framework/oidc.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod auth_oidc_service_impl;
|
@ -0,0 +1,108 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use openidconnect::{
|
||||||
|
AuthorizationCode, ClientId, ClientSecret, CsrfToken, EndpointMaybeSet, EndpointNotSet,
|
||||||
|
EndpointSet, Nonce, RedirectUrl, TokenResponse as _,
|
||||||
|
core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
adapter::gateway::{
|
||||||
|
auth_oidc_service::AuthOidcService, oidc_claims_response_dto::OidcClaimsResponseDto,
|
||||||
|
},
|
||||||
|
application::{error::auth_error::AuthError, use_case::get_auth_url_use_case::AuthUrl},
|
||||||
|
};
|
||||||
|
|
||||||
|
type CompleteClient<
|
||||||
|
HasAuthUrl = EndpointSet,
|
||||||
|
HasDeviceAuthUrl = EndpointNotSet,
|
||||||
|
HasIntrospectionUrl = EndpointNotSet,
|
||||||
|
HasRevocationUrl = EndpointNotSet,
|
||||||
|
HasTokenUrl = EndpointMaybeSet,
|
||||||
|
HasUserInfoUrl = EndpointMaybeSet,
|
||||||
|
> = CoreClient<
|
||||||
|
HasAuthUrl,
|
||||||
|
HasDeviceAuthUrl,
|
||||||
|
HasIntrospectionUrl,
|
||||||
|
HasRevocationUrl,
|
||||||
|
HasTokenUrl,
|
||||||
|
HasUserInfoUrl,
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub struct AuthOidcServiceImpl {
|
||||||
|
oidc_client: CompleteClient,
|
||||||
|
http_client: openidconnect::reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthOidcServiceImpl {
|
||||||
|
pub fn new(
|
||||||
|
provider_metadata: CoreProviderMetadata,
|
||||||
|
client_id: &str,
|
||||||
|
client_secret: &str,
|
||||||
|
redirect_url: RedirectUrl,
|
||||||
|
http_client: openidconnect::reqwest::Client,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
oidc_client: CoreClient::from_provider_metadata(
|
||||||
|
provider_metadata,
|
||||||
|
ClientId::new(client_id.to_string()),
|
||||||
|
Some(ClientSecret::new(client_secret.to_string())),
|
||||||
|
)
|
||||||
|
.set_redirect_uri(redirect_url),
|
||||||
|
http_client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AuthOidcService for AuthOidcServiceImpl {
|
||||||
|
fn get_auth_url(&self) -> Result<AuthUrl, AuthError> {
|
||||||
|
let (url, state, nonce) = self
|
||||||
|
.oidc_client
|
||||||
|
.authorize_url(
|
||||||
|
CoreAuthenticationFlow::AuthorizationCode,
|
||||||
|
CsrfToken::new_random,
|
||||||
|
Nonce::new_random,
|
||||||
|
)
|
||||||
|
.url();
|
||||||
|
|
||||||
|
Ok(AuthUrl {
|
||||||
|
url: url.to_string(),
|
||||||
|
state: state.secret().into(),
|
||||||
|
nonce: nonce.secret().into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exchange_auth_code(
|
||||||
|
&self,
|
||||||
|
code: &str,
|
||||||
|
expected_nonce: &str,
|
||||||
|
) -> Result<OidcClaimsResponseDto, AuthError> {
|
||||||
|
let token_response = self
|
||||||
|
.oidc_client
|
||||||
|
.exchange_code(AuthorizationCode::new(code.to_string()))
|
||||||
|
.map_err(|e| AuthError::OidcError(e.to_string()))?
|
||||||
|
.request_async(&self.http_client)
|
||||||
|
.await
|
||||||
|
.map_err(|_| AuthError::InvalidAuthCode)?;
|
||||||
|
|
||||||
|
let id_token = token_response.id_token().ok_or(AuthError::InvalidIdToken)?;
|
||||||
|
let claims = id_token
|
||||||
|
.claims(
|
||||||
|
&self.oidc_client.id_token_verifier(),
|
||||||
|
&Nonce::new(expected_nonce.to_string()),
|
||||||
|
)
|
||||||
|
.map_err(|_| AuthError::InvalidIdToken)?;
|
||||||
|
|
||||||
|
let preferred_username = claims
|
||||||
|
.preferred_username()
|
||||||
|
.map(|username| username.to_string());
|
||||||
|
|
||||||
|
let email = claims.email().map(|email| email.to_string());
|
||||||
|
|
||||||
|
Ok(OidcClaimsResponseDto {
|
||||||
|
sub: claims.subject().to_string(),
|
||||||
|
preferred_username: preferred_username,
|
||||||
|
email: email,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
1
backend/feature/auth/src/framework/web.rs
Normal file
1
backend/feature/auth/src/framework/web.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod auth_web_routes;
|
102
backend/feature/auth/src/framework/web/auth_web_routes.rs
Normal file
102
backend/feature/auth/src/framework/web/auth_web_routes.rs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
use actix_session::Session;
|
||||||
|
use actix_web::{HttpResponse, Responder, http::header, web};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
adapter::delivery::{
|
||||||
|
auth_controller::AuthController, oidc_callback_query_dto::OidcCallbackQueryDto,
|
||||||
|
},
|
||||||
|
application::error::auth_error::AuthError,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SESSION_KEY_AUTH_STATE: &str = "auth_state";
|
||||||
|
const SESSION_KEY_AUTH_NONCE: &str = "auth_nonce";
|
||||||
|
const SESSION_KEY_USER: &str = "user";
|
||||||
|
|
||||||
|
pub fn configure_auth_routes(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(
|
||||||
|
web::scope("/auth")
|
||||||
|
.route("/login", web::get().to(oidc_login_handler))
|
||||||
|
.route("/callback", web::get().to(oidc_callback_handler))
|
||||||
|
.route("/logout", web::get().to(logout_handler)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn oidc_login_handler(
|
||||||
|
auth_controller: web::Data<dyn AuthController>,
|
||||||
|
session: Session,
|
||||||
|
) -> impl Responder {
|
||||||
|
let result = auth_controller.oidc_login();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(auth_url) => {
|
||||||
|
if let Err(e) = session.insert(SESSION_KEY_AUTH_STATE, auth_url.state) {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
}
|
||||||
|
if let Err(e) = session.insert(SESSION_KEY_AUTH_NONCE, auth_url.nonce) {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
}
|
||||||
|
HttpResponse::Found()
|
||||||
|
.append_header((header::LOCATION, auth_url.url))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
HttpResponse::InternalServerError().finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn oidc_callback_handler(
|
||||||
|
auth_controller: web::Data<dyn AuthController>,
|
||||||
|
query: web::Query<OidcCallbackQueryDto>,
|
||||||
|
session: Session,
|
||||||
|
) -> impl Responder {
|
||||||
|
let expected_state: String = match session.get(SESSION_KEY_AUTH_STATE) {
|
||||||
|
Ok(Some(state)) => state,
|
||||||
|
_ => return HttpResponse::BadRequest().finish(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected_nonce: String = match session.get(SESSION_KEY_AUTH_NONCE) {
|
||||||
|
Ok(Some(nonce)) => nonce,
|
||||||
|
_ => return HttpResponse::BadRequest().finish(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = auth_controller
|
||||||
|
.oidc_callback(query.into_inner(), &expected_state, &expected_nonce)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
session.remove(SESSION_KEY_AUTH_STATE);
|
||||||
|
session.remove(SESSION_KEY_AUTH_NONCE);
|
||||||
|
match result {
|
||||||
|
Ok(user) => {
|
||||||
|
if let Err(e) = session.insert(SESSION_KEY_USER, user) {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
return HttpResponse::InternalServerError().finish();
|
||||||
|
}
|
||||||
|
HttpResponse::Found()
|
||||||
|
.append_header((header::LOCATION, "/"))
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
Err(e) => match e {
|
||||||
|
AuthError::InvalidAuthCode
|
||||||
|
| AuthError::InvalidIdToken
|
||||||
|
| AuthError::InvalidNonce
|
||||||
|
| AuthError::InvalidState => HttpResponse::BadRequest().finish(),
|
||||||
|
_ => {
|
||||||
|
log::error!("{e:?}");
|
||||||
|
HttpResponse::InternalServerError().finish()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn logout_handler(session: Session) -> impl Responder {
|
||||||
|
session.remove(SESSION_KEY_AUTH_STATE);
|
||||||
|
session.remove(SESSION_KEY_AUTH_NONCE);
|
||||||
|
session.remove(SESSION_KEY_USER);
|
||||||
|
HttpResponse::Found()
|
||||||
|
.append_header((header::LOCATION, "/"))
|
||||||
|
.finish()
|
||||||
|
}
|
4
backend/feature/auth/src/lib.rs
Normal file
4
backend/feature/auth/src/lib.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod adapter;
|
||||||
|
pub mod application;
|
||||||
|
pub mod domain;
|
||||||
|
pub mod framework;
|
@ -7,7 +7,6 @@ edition.workspace = true
|
|||||||
actix-multipart.workspace = true
|
actix-multipart.workspace = true
|
||||||
actix-web.workspace = true
|
actix-web.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
chrono.workspace = true
|
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
@ -4,11 +4,15 @@ version.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
actix-session.workspace = true
|
||||||
actix-web.workspace = true
|
actix-web.workspace = true
|
||||||
dotenv.workspace = true
|
dotenv.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
|
hex.workspace = true
|
||||||
|
openidconnect.workspace = true
|
||||||
percent-encoding.workspace = true
|
percent-encoding.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
|
|
||||||
|
auth.workspace = true
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
post.workspace = true
|
post.workspace = true
|
||||||
|
33
backend/server/src/configuration.rs
Normal file
33
backend/server/src/configuration.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use openidconnect::reqwest;
|
||||||
|
|
||||||
|
use crate::configuration::{
|
||||||
|
db::DbConfiguration, oidc::OidcConfiguration, server::ServerConfiguration,
|
||||||
|
session::SessionConfiguration, storage::StorageConfiguration,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod db;
|
||||||
|
pub mod oidc;
|
||||||
|
pub mod server;
|
||||||
|
pub mod session;
|
||||||
|
pub mod storage;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Configuration {
|
||||||
|
pub db: DbConfiguration,
|
||||||
|
pub oidc: OidcConfiguration,
|
||||||
|
pub server: ServerConfiguration,
|
||||||
|
pub session: SessionConfiguration,
|
||||||
|
pub storage: StorageConfiguration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configuration {
|
||||||
|
pub async fn new(http_client: reqwest::Client) -> Self {
|
||||||
|
Self {
|
||||||
|
db: DbConfiguration::new(),
|
||||||
|
oidc: OidcConfiguration::new(http_client).await,
|
||||||
|
server: ServerConfiguration::new(),
|
||||||
|
session: SessionConfiguration::new(),
|
||||||
|
storage: StorageConfiguration::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
backend/server/src/configuration/db.rs
Normal file
44
backend/server/src/configuration/db.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
use sqlx::{Pool, Postgres, postgres::PgPoolOptions};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DbConfiguration {
|
||||||
|
pub database_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DbConfiguration {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let host = env::var("DATABASE_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||||
|
let port = env::var("DATABASE_PORT").unwrap_or_else(|_| "5432".to_string());
|
||||||
|
let user = env::var("DATABASE_USER").unwrap_or_else(|_| "postgres".to_string());
|
||||||
|
let password = env::var("DATABASE_PASSWORD").unwrap_or_else(|_| "".to_string());
|
||||||
|
let dbname = env::var("DATABASE_NAME").unwrap_or_else(|_| "postgres".to_string());
|
||||||
|
|
||||||
|
let encoded_password =
|
||||||
|
percent_encoding::utf8_percent_encode(&password, percent_encoding::NON_ALPHANUMERIC)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let database_url = format!(
|
||||||
|
"postgres://{}:{}@{}:{}/{}",
|
||||||
|
user, encoded_password, host, port, dbname
|
||||||
|
);
|
||||||
|
|
||||||
|
Self { database_url }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_connection(&self) -> Pool<Postgres> {
|
||||||
|
let db_pool = PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(&self.database_url)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create database connection pool");
|
||||||
|
|
||||||
|
sqlx::migrate!("../migrations")
|
||||||
|
.run(&db_pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to run database migrations");
|
||||||
|
|
||||||
|
db_pool
|
||||||
|
}
|
||||||
|
}
|
35
backend/server/src/configuration/oidc.rs
Normal file
35
backend/server/src/configuration/oidc.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
use openidconnect::{IssuerUrl, RedirectUrl, core::CoreProviderMetadata, reqwest};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OidcConfiguration {
|
||||||
|
pub provider_metadata: CoreProviderMetadata,
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_secret: String,
|
||||||
|
pub redirect_url: RedirectUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OidcConfiguration {
|
||||||
|
pub async fn new(http_client: reqwest::Client) -> Self {
|
||||||
|
let issuer_url = env::var("OIDC_ISSUER_URL").expect("OIDC_ISSUER_URL must be set");
|
||||||
|
let client_id = env::var("OIDC_CLIENT_ID").expect("OIDC_CLIENT_ID must be set");
|
||||||
|
let client_secret = env::var("OIDC_CLIENT_SECRET").expect("OIDC_CLIENT_SECRET must be set");
|
||||||
|
let redirect_url_str = env::var("OIDC_REDIRECT_URL")
|
||||||
|
.unwrap_or_else(|_| "http://127.0.0.1:8080/auth/callback".to_string());
|
||||||
|
|
||||||
|
let provider_metadata = CoreProviderMetadata::discover_async(
|
||||||
|
IssuerUrl::new(issuer_url).expect("Invalid issuer URL"),
|
||||||
|
&http_client,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to discover OIDC provider metadata");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
provider_metadata,
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
redirect_url: RedirectUrl::new(redirect_url_str).expect("Invalid redirect URI"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
backend/server/src/configuration/server.rs
Normal file
17
backend/server/src/configuration/server.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ServerConfiguration {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerConfiguration {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||||
|
let port = std::env::var("PORT")
|
||||||
|
.unwrap_or_else(|_| "8080".to_string())
|
||||||
|
.parse::<u16>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Self { host, port }
|
||||||
|
}
|
||||||
|
}
|
36
backend/server/src/configuration/session.rs
Normal file
36
backend/server/src/configuration/session.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
use actix_session::storage::RedisSessionStore;
|
||||||
|
use actix_web::cookie::Key;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SessionConfiguration {
|
||||||
|
pub session_key: Key,
|
||||||
|
pub redis_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionConfiguration {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let session_key_hex = std::env::var("SESSION_KEY").expect("SESSION_KEY must be set");
|
||||||
|
let session_key_bytes =
|
||||||
|
hex::decode(session_key_hex).expect("Invalid SESSION_KEY format, must be hex encoded");
|
||||||
|
|
||||||
|
if session_key_bytes.len() != 64 {
|
||||||
|
panic!("SESSION_KEY must be 64 bytes long");
|
||||||
|
}
|
||||||
|
|
||||||
|
let session_key = Key::from(&session_key_bytes);
|
||||||
|
|
||||||
|
let redis_url =
|
||||||
|
std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.1:6379".to_string());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
session_key,
|
||||||
|
redis_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_session_store(&self) -> RedisSessionStore {
|
||||||
|
RedisSessionStore::new(self.redis_url.clone())
|
||||||
|
.await
|
||||||
|
.expect("Failed to create Redis session store")
|
||||||
|
}
|
||||||
|
}
|
13
backend/server/src/configuration/storage.rs
Normal file
13
backend/server/src/configuration/storage.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct StorageConfiguration {
|
||||||
|
pub storage_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StorageConfiguration {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let storage_path = env::var("STORAGE_PATH").unwrap_or_else(|_| "static".to_string());
|
||||||
|
Self { storage_path }
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,16 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use auth::{
|
||||||
|
adapter::{
|
||||||
|
delivery::auth_controller::{AuthController, AuthControllerImpl},
|
||||||
|
gateway::auth_repository_impl::AuthRepositoryImpl,
|
||||||
|
},
|
||||||
|
application::use_case::{
|
||||||
|
exchange_auth_code_use_case::ExchangeAuthCodeUseCaseImpl,
|
||||||
|
get_auth_url_use_case::LoginUseCaseImpl,
|
||||||
|
},
|
||||||
|
framework::oidc::auth_oidc_service_impl::AuthOidcServiceImpl,
|
||||||
|
};
|
||||||
use image::{
|
use image::{
|
||||||
adapter::{
|
adapter::{
|
||||||
delivery::image_controller::{ImageController, ImageControllerImpl},
|
delivery::image_controller::{ImageController, ImageControllerImpl},
|
||||||
@ -13,6 +24,7 @@ use image::{
|
|||||||
storage::image_storage_impl::ImageStorageImpl,
|
storage::image_storage_impl::ImageStorageImpl,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use openidconnect::reqwest;
|
||||||
use post::{
|
use post::{
|
||||||
adapter::{
|
adapter::{
|
||||||
delivery::post_controller::{PostController, PostControllerImpl},
|
delivery::post_controller::{PostController, PostControllerImpl},
|
||||||
@ -26,13 +38,37 @@ use post::{
|
|||||||
};
|
};
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
|
use crate::configuration::Configuration;
|
||||||
|
|
||||||
pub struct Container {
|
pub struct Container {
|
||||||
pub post_controller: Arc<dyn PostController>,
|
pub auth_controller: Arc<dyn AuthController>,
|
||||||
pub image_controller: Arc<dyn ImageController>,
|
pub image_controller: Arc<dyn ImageController>,
|
||||||
|
pub post_controller: Arc<dyn PostController>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Container {
|
impl Container {
|
||||||
pub fn new(db_pool: Pool<Postgres>, storage_path: &str) -> Self {
|
pub fn new(
|
||||||
|
db_pool: Pool<Postgres>,
|
||||||
|
http_client: reqwest::Client,
|
||||||
|
configuration: Configuration,
|
||||||
|
) -> Self {
|
||||||
|
let oidc_configuration = &configuration.oidc;
|
||||||
|
let auth_oidc_service = Arc::new(AuthOidcServiceImpl::new(
|
||||||
|
oidc_configuration.provider_metadata.clone(),
|
||||||
|
&oidc_configuration.client_id,
|
||||||
|
&oidc_configuration.client_secret,
|
||||||
|
oidc_configuration.redirect_url.clone(),
|
||||||
|
http_client,
|
||||||
|
));
|
||||||
|
let auth_repository = Arc::new(AuthRepositoryImpl::new(auth_oidc_service));
|
||||||
|
let get_auth_url_use_case = Arc::new(LoginUseCaseImpl::new(auth_repository.clone()));
|
||||||
|
let exchange_auth_code_use_case =
|
||||||
|
Arc::new(ExchangeAuthCodeUseCaseImpl::new(auth_repository.clone()));
|
||||||
|
let auth_controller = Arc::new(AuthControllerImpl::new(
|
||||||
|
get_auth_url_use_case,
|
||||||
|
exchange_auth_code_use_case,
|
||||||
|
));
|
||||||
|
|
||||||
let post_db_service = Arc::new(PostDbServiceImpl::new(db_pool.clone()));
|
let post_db_service = Arc::new(PostDbServiceImpl::new(db_pool.clone()));
|
||||||
let post_repository = Arc::new(PostRepositoryImpl::new(post_db_service.clone()));
|
let post_repository = Arc::new(PostRepositoryImpl::new(post_db_service.clone()));
|
||||||
let get_all_post_info_use_case =
|
let get_all_post_info_use_case =
|
||||||
@ -44,7 +80,7 @@ impl Container {
|
|||||||
));
|
));
|
||||||
|
|
||||||
let image_db_service = Arc::new(ImageDbServiceImpl::new(db_pool.clone()));
|
let image_db_service = Arc::new(ImageDbServiceImpl::new(db_pool.clone()));
|
||||||
let image_storage = Arc::new(ImageStorageImpl::new(storage_path.into()));
|
let image_storage = Arc::new(ImageStorageImpl::new(&configuration.storage.storage_path));
|
||||||
let image_repository = Arc::new(ImageRepositoryImpl::new(
|
let image_repository = Arc::new(ImageRepositoryImpl::new(
|
||||||
image_db_service.clone(),
|
image_db_service.clone(),
|
||||||
image_storage.clone(),
|
image_storage.clone(),
|
||||||
@ -57,8 +93,9 @@ impl Container {
|
|||||||
));
|
));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
post_controller,
|
auth_controller,
|
||||||
image_controller,
|
image_controller,
|
||||||
|
post_controller,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
|
pub mod configuration;
|
||||||
pub mod container;
|
pub mod container;
|
||||||
|
@ -1,67 +1,56 @@
|
|||||||
|
use actix_session::{
|
||||||
|
SessionMiddleware, config::SessionMiddlewareBuilder, storage::RedisSessionStore,
|
||||||
|
};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
App, Error, HttpServer,
|
App, Error, HttpServer,
|
||||||
body::MessageBody,
|
body::MessageBody,
|
||||||
dev::{ServiceFactory, ServiceRequest, ServiceResponse},
|
dev::{ServiceFactory, ServiceRequest, ServiceResponse},
|
||||||
web,
|
web,
|
||||||
};
|
};
|
||||||
|
use auth::framework::web::auth_web_routes::configure_auth_routes;
|
||||||
use image::framework::web::image_web_routes::configure_image_routes;
|
use image::framework::web::image_web_routes::configure_image_routes;
|
||||||
|
use openidconnect::reqwest;
|
||||||
use post::framework::web::post_web_routes::configure_post_routes;
|
use post::framework::web::post_web_routes::configure_post_routes;
|
||||||
use server::container::Container;
|
use server::{configuration::Configuration, container::Container};
|
||||||
use sqlx::{Pool, Postgres, postgres::PgPoolOptions};
|
use sqlx::{Pool, Postgres};
|
||||||
use std::env;
|
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
let db_pool = init_database().await;
|
let http_client = reqwest::ClientBuilder::new()
|
||||||
let storage_path = env::var("STORAGE_PATH").unwrap_or_else(|_| "static".to_string());
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create HTTP client");
|
||||||
|
|
||||||
let host = env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
|
let configuration = Configuration::new(http_client.clone()).await;
|
||||||
let port = env::var("PORT")
|
|
||||||
.unwrap_or_else(|_| "8080".to_string())
|
|
||||||
.parse::<u16>()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
HttpServer::new(move || create_app(db_pool.clone(), storage_path.clone()))
|
let host = configuration.server.host.clone();
|
||||||
.bind((host, port))?
|
let port = configuration.server.port;
|
||||||
.run()
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn init_database() -> Pool<Postgres> {
|
let db_pool = configuration.db.create_connection().await;
|
||||||
let host = env::var("DATABASE_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
let session_key = configuration.session.session_key.clone();
|
||||||
let port = env::var("DATABASE_PORT").unwrap_or_else(|_| "5432".to_string());
|
let session_store = configuration.session.create_session_store().await;
|
||||||
let user = env::var("DATABASE_USER").unwrap_or_else(|_| "postgres".to_string());
|
|
||||||
let password = env::var("DATABASE_PASSWORD").unwrap_or_else(|_| "".to_string());
|
|
||||||
let dbname = env::var("DATABASE_NAME").unwrap_or_else(|_| "postgres".to_string());
|
|
||||||
|
|
||||||
let encoded_password =
|
HttpServer::new(move || {
|
||||||
percent_encoding::utf8_percent_encode(&password, percent_encoding::NON_ALPHANUMERIC)
|
create_app(
|
||||||
.to_string();
|
db_pool.clone(),
|
||||||
let database_url = format!(
|
http_client.clone(),
|
||||||
"postgres://{}:{}@{}:{}/{}",
|
SessionMiddleware::builder(session_store.clone(), session_key.clone()),
|
||||||
user, encoded_password, host, port, dbname
|
configuration.clone(),
|
||||||
);
|
)
|
||||||
|
})
|
||||||
let db_pool = PgPoolOptions::new()
|
.bind((host, port))?
|
||||||
.max_connections(5)
|
.run()
|
||||||
.connect(&database_url)
|
.await
|
||||||
.await
|
|
||||||
.expect("Failed to create database connection pool");
|
|
||||||
|
|
||||||
sqlx::migrate!("../migrations")
|
|
||||||
.run(&db_pool)
|
|
||||||
.await
|
|
||||||
.expect("Failed to run database migrations");
|
|
||||||
|
|
||||||
db_pool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_app(
|
fn create_app(
|
||||||
db_pool: Pool<Postgres>,
|
db_pool: Pool<Postgres>,
|
||||||
storage_path: String,
|
http_client: reqwest::Client,
|
||||||
|
session_middleware_builder: SessionMiddlewareBuilder<RedisSessionStore>,
|
||||||
|
configuration: Configuration,
|
||||||
) -> App<
|
) -> App<
|
||||||
impl ServiceFactory<
|
impl ServiceFactory<
|
||||||
ServiceRequest,
|
ServiceRequest,
|
||||||
@ -71,11 +60,14 @@ fn create_app(
|
|||||||
Error = Error,
|
Error = Error,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
let container = Container::new(db_pool, &storage_path);
|
let container = Container::new(db_pool, http_client, configuration);
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::from(container.post_controller))
|
.wrap(session_middleware_builder.build())
|
||||||
|
.app_data(web::Data::from(container.auth_controller))
|
||||||
.app_data(web::Data::from(container.image_controller))
|
.app_data(web::Data::from(container.image_controller))
|
||||||
.configure(configure_post_routes)
|
.app_data(web::Data::from(container.post_controller))
|
||||||
|
.configure(configure_auth_routes)
|
||||||
.configure(configure_image_routes)
|
.configure(configure_image_routes)
|
||||||
|
.configure(configure_post_routes)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user