mirror of
https://github.com/clowzed/sero
synced 2024-06-04 23:52:13 +02:00
Added cors layer. Added entity and migrations to store origins. Added endpoints for manipulation origins. Automatically clear origins on teardown and upload implemented. Added endpoints spec to openapi.yml. Untested
This commit is contained in:
parent
f892823d0a
commit
e75544f0a4
25
Cargo.lock
generated
25
Cargo.lock
generated
|
@ -1357,6 +1357,12 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.8.0"
|
||||
|
@ -2625,6 +2631,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-postgres",
|
||||
"tokio-util",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
|
@ -3267,6 +3274,24 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
|
||||
dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-range-header",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.2"
|
||||
|
|
|
@ -32,6 +32,7 @@ bytes = "1.5.0"
|
|||
async-trait = "0.1.73"
|
||||
tokio-util = { version = "0.7.9", features = ["io"] }
|
||||
dotenv = "0.15.0"
|
||||
tower-http = { version = "0.4.4", features = ["cors"] }
|
||||
|
||||
[workspace]
|
||||
members = [".", "entity", "migration"]
|
||||
|
|
38
entity/src/cors.rs
Normal file
38
entity/src/cors.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "cors")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub subdomain_id: i32,
|
||||
pub origin: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::subdomain::Entity",
|
||||
from = "Column::SubdomainId",
|
||||
to = "super::subdomain::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Subdomain,
|
||||
}
|
||||
|
||||
impl Related<super::subdomain::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Subdomain.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl Model {
|
||||
pub fn matches(&self, origin: &str) -> bool {
|
||||
self.origin == "*" || self.origin == origin
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
pub mod prelude;
|
||||
|
||||
pub mod cors;
|
||||
pub mod file;
|
||||
pub mod subdomain;
|
||||
pub mod user;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod cors;
|
||||
pub mod file;
|
||||
pub mod subdomain;
|
||||
pub mod user;
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
pub use super::cors::Entity as CorsEntity;
|
||||
pub use super::file::Entity as FileEntity;
|
||||
pub use super::subdomain::Entity as SubdomainEntity;
|
||||
pub use super::user::Entity as UserEntity;
|
||||
|
||||
pub use super::cors::ActiveModel as ActiveCors;
|
||||
pub use super::file::ActiveModel as ActiveFile;
|
||||
pub use super::subdomain::ActiveModel as ActiveSubdomain;
|
||||
pub use super::user::ActiveModel as ActiveUser;
|
||||
|
||||
pub use super::cors::Model as Cors;
|
||||
pub use super::file::Model as File;
|
||||
pub use super::subdomain::Model as Subdomain;
|
||||
pub use super::user::Model as User;
|
||||
|
||||
pub use super::cors::Column as CorsColumn;
|
||||
pub use super::file::Column as FileColumn;
|
||||
pub use super::subdomain::Column as SubdomainColumn;
|
||||
pub use super::user::Column as UserColumn;
|
||||
|
|
|
@ -17,6 +17,8 @@ pub struct Model {
|
|||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::cors::Entity")]
|
||||
Cors,
|
||||
#[sea_orm(has_many = "super::file::Entity")]
|
||||
File,
|
||||
#[sea_orm(
|
||||
|
@ -29,6 +31,12 @@ pub enum Relation {
|
|||
User,
|
||||
}
|
||||
|
||||
impl Related<super::cors::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Cors.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::file::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::File.def()
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "user")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
|
@ -27,13 +25,3 @@ impl Related<super::subdomain::Entity> for Entity {
|
|||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl Debug for Model {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Model")
|
||||
.field("id", &self.id)
|
||||
.field("username", &self.username)
|
||||
.field("password", &"...")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ pub use sea_orm_migration::prelude::*;
|
|||
mod m20230927_162921_create_users;
|
||||
mod m20230929_081415_create_subdomains;
|
||||
mod m20230929_152215_create_files;
|
||||
mod m20231105_171000_create_cors;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
|
@ -13,6 +14,7 @@ impl MigratorTrait for Migrator {
|
|||
Box::new(m20230927_162921_create_users::Migration),
|
||||
Box::new(m20230929_081415_create_subdomains::Migration),
|
||||
Box::new(m20230929_152215_create_files::Migration),
|
||||
Box::new(m20231105_171000_create_cors::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
49
migration/src/m20231105_171000_create_cors.rs
Normal file
49
migration/src/m20231105_171000_create_cors.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use crate::m20230929_081415_create_subdomains::Subdomain;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Cors::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Cors::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Cors::SubdomainId).integer().not_null())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(Cors::Table, Cors::SubdomainId)
|
||||
.to(Subdomain::Table, Subdomain::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.col(ColumnDef::new(Cors::Origin).string().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Cors::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum Cors {
|
||||
Table,
|
||||
Id,
|
||||
SubdomainId,
|
||||
Origin,
|
||||
}
|
79
openapi.yml
79
openapi.yml
|
@ -294,11 +294,6 @@ paths:
|
|||
responses:
|
||||
"200":
|
||||
description: Site was successfully uploaded
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
|
||||
"403":
|
||||
description: Subdomain is owned by another user
|
||||
|
@ -324,6 +319,74 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
/api/cors/add:
|
||||
post:
|
||||
summary: Add origin
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Origin"
|
||||
responses:
|
||||
"200":
|
||||
description: Origin was successfully added
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"401":
|
||||
description: Authentication failed, unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"403":
|
||||
description: Subdomain is owned by another user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"500":
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
/api/cors/clear:
|
||||
post:
|
||||
summary: Clear related origins
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Origins were successfully removed
|
||||
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"401":
|
||||
description: Authentication failed, unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"403":
|
||||
description: Subdomain is owned by another user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"500":
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
|
@ -344,6 +407,12 @@ components:
|
|||
properties:
|
||||
details:
|
||||
type: string
|
||||
Origin:
|
||||
type: object
|
||||
properties:
|
||||
origin:
|
||||
type: string
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
|
|
156
src/apperror.rs
156
src/apperror.rs
|
@ -25,109 +25,67 @@ pub enum SeroError {
|
|||
EmptyCredentials,
|
||||
}
|
||||
|
||||
impl IntoResponse for SeroError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let response = match self {
|
||||
SeroError::XSubdomainHeaderMissing => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(Details {
|
||||
details: "X-Subdomain header is missing!".into(),
|
||||
}),
|
||||
),
|
||||
SeroError::AuthorizationHeaderMissing => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(Details {
|
||||
details: "Authorization header is missing!".into(),
|
||||
}),
|
||||
),
|
||||
SeroError::AuthorizationHeaderBadSchema => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(Details {
|
||||
details: "Authorization header does not match schema!
|
||||
Required schema: Authorization: Bearer <token>"
|
||||
.into(),
|
||||
}),
|
||||
),
|
||||
SeroError::SubdomainIsOwnedByAnotherUser(subdomain_name) => (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(Details {
|
||||
details: format!(
|
||||
"Subdomain with name {} is owned by another user!",
|
||||
subdomain_name
|
||||
),
|
||||
}),
|
||||
),
|
||||
SeroError::AuthorizationHeaderBabChars => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(Details {
|
||||
details: "Authorization header contains invalid characters!".into(),
|
||||
}),
|
||||
),
|
||||
impl std::fmt::Debug for SeroError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", match self {
|
||||
SeroError::XSubdomainHeaderMissing => "X-Subdomain header is missing!".to_string(),
|
||||
SeroError::AuthorizationHeaderMissing => "Authorization header is missing!".to_string(),
|
||||
SeroError::AuthorizationHeaderBadSchema => "Authorization header does not match schema! Required schema: Authorization: Bearer <token>".to_string(),
|
||||
SeroError::SubdomainIsOwnedByAnotherUser(subdomain_name) => format!("Subdomain with name {} is owned by another user!", subdomain_name),
|
||||
SeroError::AuthorizationHeaderBabChars => "Authorization header contains invalid characters!".to_string(),
|
||||
SeroError::InternalServerError(cause) => {
|
||||
tracing::error!(%cause, "Error!");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(Details {
|
||||
details: "Some error occurred on the server!".into(),
|
||||
}),
|
||||
)
|
||||
"Some error occurred on the server!".to_string()
|
||||
},
|
||||
SeroError::UserWasNotFoundUsingJwt => "User with id from jwt token was not found!".to_string(),
|
||||
SeroError::RegisteredUserLimitExceeded => "Registered user limit exceeded!".to_string(),
|
||||
SeroError::Unauthorized => "Unauthorized! Bad credentials were provided!".to_string(),
|
||||
SeroError::UserHasAlreadyBeenRegistered => "User with this username has already been registered!".to_string(),
|
||||
SeroError::SubdomainWasNotFound(subdomain_name) => format!("Subdomain with name {} was not found!", subdomain_name),
|
||||
SeroError::ArchiveFileWasNotFoundForSubdomain(subdomain_name) => format!("Archive file was not found for subdomain {}", subdomain_name),
|
||||
SeroError::MaxSitesPerUserLimitExceeded => "Max sites per this user limit exceeded!".to_string(),
|
||||
SeroError::SiteDisabled => "Site is disabled!".to_string(),
|
||||
SeroError::EmptyCredentials => "Empty credentials were provided!".to_string(),
|
||||
})
|
||||
}
|
||||
SeroError::UserWasNotFoundUsingJwt => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(Details {
|
||||
details: "User with id from jwt token was not found!".into(),
|
||||
}),
|
||||
),
|
||||
SeroError::RegisteredUserLimitExceeded => (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(Details {
|
||||
details: "Registered user limit exceeded!".into(),
|
||||
}),
|
||||
),
|
||||
SeroError::Unauthorized => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(Details {
|
||||
details: "Unauthorized! Bad credentials were provided!".into(),
|
||||
}),
|
||||
),
|
||||
SeroError::UserHasAlreadyBeenRegistered => (
|
||||
StatusCode::CONFLICT,
|
||||
Json(Details {
|
||||
details: "User with this username has already been registered!".into(),
|
||||
}),
|
||||
),
|
||||
SeroError::SubdomainWasNotFound(subdomain_name) => (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(Details {
|
||||
details: format!("Subdomain with name {subdomain_name} was not found!"),
|
||||
}),
|
||||
),
|
||||
SeroError::ArchiveFileWasNotFoundForSubdomain(subdomain_name) => (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(Details {
|
||||
details: format!("Archive file was not found for subdomain {subdomain_name}"),
|
||||
}),
|
||||
),
|
||||
SeroError::MaxSitesPerUserLimitExceeded => (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(Details {
|
||||
details: "Max sites per this user limit exceeded!".into(),
|
||||
}),
|
||||
),
|
||||
SeroError::SiteDisabled => (
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
Json(Details {
|
||||
details: "Service is currently unavailable!".into(),
|
||||
}),
|
||||
),
|
||||
SeroError::EmptyCredentials => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(Details {
|
||||
details: "Username or password is empty!".into(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SeroError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SeroError> for StatusCode {
|
||||
fn from(val: &SeroError) -> Self {
|
||||
match val {
|
||||
SeroError::XSubdomainHeaderMissing => StatusCode::BAD_REQUEST,
|
||||
SeroError::AuthorizationHeaderMissing => StatusCode::BAD_REQUEST,
|
||||
SeroError::AuthorizationHeaderBadSchema => StatusCode::BAD_REQUEST,
|
||||
SeroError::SubdomainIsOwnedByAnotherUser(_) => StatusCode::FORBIDDEN,
|
||||
SeroError::AuthorizationHeaderBabChars => StatusCode::BAD_REQUEST,
|
||||
SeroError::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
SeroError::UserWasNotFoundUsingJwt => StatusCode::UNAUTHORIZED,
|
||||
SeroError::RegisteredUserLimitExceeded => StatusCode::FORBIDDEN,
|
||||
SeroError::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
SeroError::UserHasAlreadyBeenRegistered => StatusCode::CONFLICT,
|
||||
SeroError::SubdomainWasNotFound(_) => StatusCode::NOT_FOUND,
|
||||
SeroError::ArchiveFileWasNotFoundForSubdomain(_) => StatusCode::NOT_FOUND,
|
||||
SeroError::MaxSitesPerUserLimitExceeded => StatusCode::FORBIDDEN,
|
||||
SeroError::SiteDisabled => StatusCode::SERVICE_UNAVAILABLE,
|
||||
SeroError::EmptyCredentials => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for SeroError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let response = (
|
||||
Into::<StatusCode>::into(&self),
|
||||
Json(Details {
|
||||
details: format!("{:?}", self),
|
||||
}),
|
||||
);
|
||||
tracing::error!(cause = response.1.details, "Response with error!");
|
||||
response.into_response()
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ impl Default for Config {
|
|||
let mut config: Self =
|
||||
envy::from_env().expect("Failed to read config from environment variables!");
|
||||
if config.jwt_secret.is_none() {
|
||||
config.jwt_secret = Some(uuid::Uuid::new_v4().to_string())
|
||||
config.jwt_secret = Some(uuid::Uuid::new_v4().to_string());
|
||||
}
|
||||
config
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use axum::{
|
||||
async_trait,
|
||||
extract::{FromRef, FromRequestParts},
|
||||
http::request::Parts,
|
||||
http::{request::Parts, HeaderMap},
|
||||
};
|
||||
use sea_orm::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
@ -59,19 +59,10 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for Subdomain
|
||||
where
|
||||
Arc<crate::AppState>: FromRef<S>,
|
||||
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = SeroError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
impl Subdomain {
|
||||
pub fn from_headers(headers: &HeaderMap) -> Result<Self, SeroError> {
|
||||
Ok(Self({
|
||||
let header = parts
|
||||
.headers
|
||||
let header = headers
|
||||
.get("X-Subdomain")
|
||||
.ok_or(SeroError::XSubdomainHeaderMissing)?
|
||||
.to_str()
|
||||
|
@ -86,6 +77,20 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for Subdomain
|
||||
where
|
||||
Arc<crate::AppState>: FromRef<S>,
|
||||
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = SeroError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
Self::from_headers(&parts.headers)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for SubdomainModel
|
||||
where
|
||||
|
@ -96,10 +101,20 @@ where
|
|||
|
||||
#[tracing::instrument(skip(parts, state))]
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
Self::from_headers(&parts.headers, state).await
|
||||
}
|
||||
}
|
||||
|
||||
impl SubdomainModel {
|
||||
pub async fn from_headers<S>(headers: &HeaderMap, state: &S) -> Result<Self, SeroError>
|
||||
where
|
||||
Arc<crate::AppState>: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
let app_state = Arc::from_ref(state);
|
||||
|
||||
let subdomain_name = Subdomain::from_request_parts(parts, state).await?.0;
|
||||
Ok(match entity::prelude::SubdomainEntity::find()
|
||||
let subdomain_name = Subdomain::from_headers(headers)?.0;
|
||||
match entity::prelude::SubdomainEntity::find()
|
||||
.filter(entity::prelude::SubdomainColumn::Name.eq(&subdomain_name))
|
||||
.one(&app_state.connection)
|
||||
.await
|
||||
|
@ -107,7 +122,7 @@ where
|
|||
Ok(Some(subdomain)) => Ok(Self(subdomain)),
|
||||
Ok(None) => Err(SeroError::SubdomainWasNotFound(subdomain_name)),
|
||||
Err(cause) => Err(SeroError::InternalServerError(Box::new(cause))),
|
||||
}?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
66
src/handlers/cors.rs
Normal file
66
src/handlers/cors.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
use crate::{
|
||||
apperror::SeroError,
|
||||
extractors::{AuthJWT, SubdomainModel},
|
||||
AppState,
|
||||
};
|
||||
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Form,
|
||||
};
|
||||
use entity::prelude::*;
|
||||
use sea_orm::{prelude::*, Set};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct OriginForm {
|
||||
origin: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub async fn add_origin(
|
||||
State(state): State<Arc<AppState>>,
|
||||
SubdomainModel(subdomain_model): SubdomainModel,
|
||||
AuthJWT(user): AuthJWT,
|
||||
Form(origin_form): Form<OriginForm>,
|
||||
) -> Response {
|
||||
if subdomain_model.owner_id != user.id {
|
||||
return SeroError::SubdomainIsOwnedByAnotherUser(subdomain_model.name).into_response();
|
||||
}
|
||||
|
||||
let active_cors_origin = ActiveCors {
|
||||
origin: Set(origin_form.origin),
|
||||
subdomain_id: Set(subdomain_model.id),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
match CorsEntity::insert(active_cors_origin)
|
||||
.exec(&state.connection)
|
||||
.await
|
||||
{
|
||||
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(cause) => SeroError::InternalServerError(Box::new(cause)).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub async fn clear_all(
|
||||
State(state): State<Arc<AppState>>,
|
||||
SubdomainModel(subdomain_model): SubdomainModel,
|
||||
AuthJWT(user): AuthJWT,
|
||||
) -> Response {
|
||||
if subdomain_model.owner_id != user.id {
|
||||
return SeroError::SubdomainIsOwnedByAnotherUser(subdomain_model.name).into_response();
|
||||
}
|
||||
|
||||
match CorsEntity::delete_many()
|
||||
.filter(CorsColumn::SubdomainId.eq(subdomain_model.id))
|
||||
.exec(&state.connection)
|
||||
.await
|
||||
{
|
||||
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(cause) => SeroError::InternalServerError(Box::new(cause)).into_response(),
|
||||
}
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
pub mod auth;
|
||||
pub mod cors;
|
||||
pub mod sites;
|
||||
|
|
48
src/main.rs
48
src/main.rs
|
@ -2,10 +2,14 @@ use std::{fmt::Debug, net::SocketAddr};
|
|||
|
||||
use axum::{
|
||||
extract::DefaultBodyLimit,
|
||||
http::StatusCode,
|
||||
http::{request::Parts, HeaderName, HeaderValue, Method, StatusCode},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use extractors::SubdomainModel;
|
||||
use services::cors::CorsService;
|
||||
use tower_http::cors::{AllowHeaders, AllowMethods, AllowOrigin, CorsLayer};
|
||||
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use sea_orm::{ConnectOptions, Database};
|
||||
|
||||
|
@ -47,17 +51,53 @@ async fn main() {
|
|||
.route("/teardown", post(handlers::sites::teardown))
|
||||
.route("/download", post(handlers::sites::download))
|
||||
.route("/enable", post(handlers::sites::enable))
|
||||
.route("/disable", post(handlers::sites::disable));
|
||||
.route("/disable", post(handlers::sites::disable))
|
||||
.route("/cors/add", post(handlers::cors::add_origin))
|
||||
.route("/cors/clear", post(handlers::cors::clear_all));
|
||||
|
||||
let state = std::sync::Arc::new(AppState {
|
||||
connection,
|
||||
config: Default::default(),
|
||||
});
|
||||
|
||||
let mut app = Router::new()
|
||||
.nest("/api", api_router)
|
||||
let cloned_state = state.clone();
|
||||
|
||||
let files_router = Router::new()
|
||||
.route("/*path", get(handlers::sites::file))
|
||||
.route("/", get(handlers::sites::index_redirect))
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_methods(AllowMethods::exact(Method::GET))
|
||||
.allow_headers(AllowHeaders::list([HeaderName::from_static("X-Subdomain")]))
|
||||
.allow_origin(AllowOrigin::predicate(
|
||||
move |origin: &HeaderValue, parts: &Parts| {
|
||||
let origin = origin.to_str().unwrap_or_default();
|
||||
let subdomain_model_future =
|
||||
SubdomainModel::from_headers(&parts.headers, &cloned_state);
|
||||
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
let subdomain_model = subdomain_model_future.await;
|
||||
match subdomain_model {
|
||||
Ok(model) => match CorsService::check(model.0, origin, &cloned_state.connection).await{
|
||||
Ok(result) => result,
|
||||
Err(cause) => {
|
||||
tracing::error!(%cause, "Failed to check origin for cors filtering!");
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(cause) => {
|
||||
tracing::error!(%cause, "Failed to find subdomain model for cors filtering!");
|
||||
false
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
)),
|
||||
);
|
||||
|
||||
let mut app = Router::new()
|
||||
.nest("/api", api_router)
|
||||
.nest("/", files_router)
|
||||
.with_state(state.clone());
|
||||
|
||||
if config.max_body_limit_size.is_some() {
|
||||
|
|
22
src/services/cors.rs
Normal file
22
src/services/cors.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use entity::prelude::*;
|
||||
use sea_orm::{ConnectionTrait, DbErr, ModelTrait};
|
||||
|
||||
use sea_orm::TransactionTrait;
|
||||
|
||||
pub struct CorsService;
|
||||
|
||||
impl CorsService {
|
||||
#[tracing::instrument(skip(connection))]
|
||||
pub async fn check<T: ConnectionTrait + TransactionTrait>(
|
||||
subdomain: Subdomain,
|
||||
origin: &str,
|
||||
connection: &T,
|
||||
) -> Result<bool, DbErr> {
|
||||
Ok(subdomain
|
||||
.find_related(CorsEntity)
|
||||
.all(connection)
|
||||
.await?
|
||||
.iter()
|
||||
.any(|origin_model| origin_model.matches(origin)))
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
pub mod archive;
|
||||
pub mod auth;
|
||||
pub mod cors;
|
||||
pub mod sites;
|
||||
pub mod users;
|
||||
|
|
|
@ -88,6 +88,11 @@ impl SitesService {
|
|||
}
|
||||
};
|
||||
|
||||
CorsEntity::delete_many()
|
||||
.filter(CorsColumn::SubdomainId.eq(subdomain.id))
|
||||
.exec(connection)
|
||||
.await?;
|
||||
|
||||
match new_archive_file.write_all(&contents).await {
|
||||
Ok(()) => {
|
||||
let mut active: ActiveSubdomain = subdomain.clone().into();
|
||||
|
|
Loading…
Reference in a new issue