mirror of
https://github.com/clowzed/sero
synced 2024-06-15 21:05:00 +02:00
commit
0469257a59
27
.github/workflows/testable.yml
vendored
Normal file
27
.github/workflows/testable.yml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: docker build testable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: clowzed
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: clowzed/sero:testable
|
63
Cargo.lock
generated
63
Cargo.lock
generated
|
@ -1112,12 +1112,13 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
|||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
|
||||
checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
|
@ -1126,9 +1127,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
|
||||
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
|
@ -1136,15 +1137,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
||||
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
|
||||
checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
|
@ -1164,9 +1165,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
|
||||
checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
|
@ -1185,9 +1186,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
||||
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1196,21 +1197,21 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
|
||||
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
||||
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.28"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
||||
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
|
@ -1419,6 +1420,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"
|
||||
|
@ -2853,6 +2860,7 @@ dependencies = [
|
|||
"dotenv",
|
||||
"entity",
|
||||
"envy",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"hyper",
|
||||
"jsonwebtoken",
|
||||
|
@ -2868,6 +2876,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-postgres",
|
||||
"tokio-util",
|
||||
"tower-http",
|
||||
"tower",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
@ -3542,6 +3551,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,8 @@ 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"] }
|
||||
futures = "0.3.29"
|
||||
tower = { version = "0.4.13", features = ["util"] }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde_json = "1.0.108"
|
||||
|
|
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,
|
||||
}
|
96
openapi.yml
96
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,91 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
/api/cors/add:
|
||||
post:
|
||||
summary: Add origin
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Subdomain
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
security:
|
||||
- bearerAuth: []
|
||||
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
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Subdomain
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
|
||||
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 +424,12 @@ components:
|
|||
properties:
|
||||
details:
|
||||
type: string
|
||||
Origin:
|
||||
type: object
|
||||
properties:
|
||||
origin:
|
||||
type: string
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
|
|
158
src/apperror.rs
158
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(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
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(),
|
||||
}),
|
||||
),
|
||||
};
|
||||
"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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
58
src/main.rs
58
src/main.rs
|
@ -1,4 +1,4 @@
|
|||
use std::{fmt::Debug, net::SocketAddr};
|
||||
use std::{fmt::Debug, net::SocketAddr, sync::mpsc};
|
||||
|
||||
use axum::{
|
||||
extract::DefaultBodyLimit,
|
||||
|
@ -6,6 +6,11 @@ use axum::{
|
|||
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};
|
||||
|
||||
|
@ -39,13 +44,62 @@ async fn app() -> Router {
|
|||
.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 cloned_state = state.clone();
|
||||
|
||||
let cors_layer = CorsLayer::new()
|
||||
.allow_methods(AllowMethods::any())
|
||||
.allow_headers(AllowHeaders::any())
|
||||
.allow_origin(AllowOrigin::predicate(move |origin, parts| {
|
||||
let cloned_headers = parts.headers.clone();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let cloned_state = cloned_state.clone();
|
||||
let cloned_origin = origin
|
||||
.to_owned()
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async move {
|
||||
tracing::info!("Starting cors!");
|
||||
let subdomain_model_extractor =
|
||||
SubdomainModel::from_headers(&cloned_headers, &cloned_state)
|
||||
.await
|
||||
.map_err(|cause| {
|
||||
tracing::error!(%cause,
|
||||
"Failed to extract subdomain model from headers for cors!");
|
||||
});
|
||||
if subdomain_model_extractor.is_err() {
|
||||
tx.send(false).ok();
|
||||
return;
|
||||
}
|
||||
|
||||
let res = CorsService::check(
|
||||
subdomain_model_extractor.unwrap().0,
|
||||
&cloned_origin,
|
||||
&cloned_state.connection,
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
tx.send(res).ok();
|
||||
});
|
||||
});
|
||||
|
||||
rx.recv().unwrap_or(false)
|
||||
}));
|
||||
|
||||
let mut app = Router::new()
|
||||
.nest("/api", api_router)
|
||||
.route("/*path", get(handlers::sites::file))
|
||||
|
|
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