Add initial version of controller
This commit is contained in:
parent
f177d01df6
commit
5fe90e57eb
40 changed files with 6900 additions and 4 deletions
2793
Cargo.lock
generated
Normal file
2793
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
46
Cargo.toml
Normal file
46
Cargo.toml
Normal file
|
@ -0,0 +1,46 @@
|
|||
[package]
|
||||
name = "driptorch-controller"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
sea-orm = { version = "0.9.2", features = [ "sqlx-postgres", "runtime-tokio-rustls", "macros" ] }
|
||||
sea-orm-migration = "0.9.2"
|
||||
migration = { version = "0.1.0", path = "./migration"}
|
||||
|
||||
futures = "0.3.24"
|
||||
dotenv = "0.15.0"
|
||||
|
||||
serde = { version = "1.0.144", features = ["derive"] }
|
||||
rmp = "0.8.11"
|
||||
rmp-serde = "1.1.0"
|
||||
|
||||
ulid = "1.0.0"
|
||||
|
||||
axum = "0.6.0-rc.1"
|
||||
tower = "0.4.13"
|
||||
|
||||
blake3 = "1.3.1"
|
||||
base64ct = { version = "1.5.2", features = ["alloc"] }
|
||||
rand = "0.8.5"
|
||||
|
||||
argon2 = "0.4.1"
|
||||
|
||||
tokio = { version = "1.21.0", features = ["full"] }
|
||||
|
||||
pretty_env_logger = "0.4.0"
|
||||
log = "0.4.17"
|
||||
|
||||
chrono = "0.4.22"
|
||||
|
||||
zxcvbn = "2.2.1"
|
||||
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
regex = "1.6.0"
|
||||
|
||||
async-trait = "0.1.57"
|
||||
|
||||
user-agent-parser = "0.3.3"
|
|
@ -5,10 +5,10 @@ For more information, visit https://driptorch.net/
|
|||
---
|
||||
|
||||
## Requirements:
|
||||
### Deployment
|
||||
#### Deployment
|
||||
* PostgreSQL
|
||||
|
||||
### Development
|
||||
#### Development
|
||||
* Rust 1.60+
|
||||
* PostgreSQL
|
||||
|
||||
|
@ -18,10 +18,8 @@ For more information, visit https://driptorch.net/
|
|||
| DATABASE_URL | PostgreSQL database connection URL | | Y |
|
||||
| UAP_REGEXES | Path to the [BrowserScope UA regex YAML](https://github.com/ua-parser/uap-core/blob/master/regexes.yaml) | | N |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### See also
|
||||
|
||||
* [driptorch-client](https://git.sr.ht/~eviee/driptorch-client)
|
||||
* [driptorch-panel](https://git.sr.ht/~eviee/driptorch-panel)
|
2051
migration/Cargo.lock
generated
Normal file
2051
migration/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
19
migration/Cargo.toml
Normal file
19
migration/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "migration"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "migration"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
async-std = { version = "^1", features = ["attributes", "tokio1"] }
|
||||
|
||||
[dependencies.sea-orm-migration]
|
||||
version = "^0.9.0"
|
||||
features = [
|
||||
"sqlx-postgres",
|
||||
"runtime-tokio-rustls"
|
||||
]
|
41
migration/README.md
Normal file
41
migration/README.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
# Running Migrator CLI
|
||||
|
||||
- Generate a new migration file
|
||||
```sh
|
||||
cargo run -- migrate generate MIGRATION_NAME
|
||||
```
|
||||
- Apply all pending migrations
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
```sh
|
||||
cargo run -- up
|
||||
```
|
||||
- Apply first 10 pending migrations
|
||||
```sh
|
||||
cargo run -- up -n 10
|
||||
```
|
||||
- Rollback last applied migrations
|
||||
```sh
|
||||
cargo run -- down
|
||||
```
|
||||
- Rollback last 10 applied migrations
|
||||
```sh
|
||||
cargo run -- down -n 10
|
||||
```
|
||||
- Drop all tables from the database, then reapply all migrations
|
||||
```sh
|
||||
cargo run -- fresh
|
||||
```
|
||||
- Rollback all applied migrations, then reapply all migrations
|
||||
```sh
|
||||
cargo run -- refresh
|
||||
```
|
||||
- Rollback all applied migrations
|
||||
```sh
|
||||
cargo run -- reset
|
||||
```
|
||||
- Check the status of all migrations
|
||||
```sh
|
||||
cargo run -- status
|
||||
```
|
28
migration/src/lib.rs
Normal file
28
migration/src/lib.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
pub use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20220907_223615_create_users;
|
||||
mod m20220907_223632_create_sessions;
|
||||
mod m20220907_223637_create_zones;
|
||||
mod m20220907_223639_create_records;
|
||||
mod m20220907_223653_create_proxies;
|
||||
mod m20220907_223633_create_teams;
|
||||
mod m20220907_223634_create_team_members;
|
||||
mod m20220908_204553_create_clients;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(m20220907_223615_create_users::Migration),
|
||||
Box::new(m20220907_223632_create_sessions::Migration),
|
||||
Box::new(m20220907_223633_create_teams::Migration),
|
||||
Box::new(m20220907_223634_create_team_members::Migration),
|
||||
Box::new(m20220907_223637_create_zones::Migration),
|
||||
Box::new(m20220907_223639_create_records::Migration),
|
||||
Box::new(m20220907_223653_create_proxies::Migration),
|
||||
Box::new(m20220908_204553_create_clients::Migration),
|
||||
]
|
||||
}
|
||||
}
|
69
migration/src/m20220907_223615_create_users.rs
Normal file
69
migration/src/m20220907_223615_create_users.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20220907_223615_create_users"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(User::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(User::Id)
|
||||
.string()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
)
|
||||
.col(ColumnDef::new(User::Name)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.col(ColumnDef::new(User::Email)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key()
|
||||
)
|
||||
.col(ColumnDef::new(User::Password)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.col(ColumnDef::new(User::Active)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true)
|
||||
)
|
||||
.col(ColumnDef::new(User::Admin)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false)
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(User::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
pub enum User {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Email,
|
||||
Password,
|
||||
Active,
|
||||
Admin
|
||||
}
|
74
migration/src/m20220907_223632_create_sessions.rs
Normal file
74
migration/src/m20220907_223632_create_sessions.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use super::m20220907_223615_create_users::User;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20220907_223632_create_sessions"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Session::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(Session::Id)
|
||||
.string()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
)
|
||||
.col(ColumnDef::new(Session::Name)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.col(ColumnDef::new(Session::Ip)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.col(ColumnDef::new(Session::Token)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.col(ColumnDef::new(Session::Context)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.foreign_key(ForeignKey::create()
|
||||
.name("fk-sessions-user-id")
|
||||
.from(Session::Table, Session::Context)
|
||||
.to(User::Table, User::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
)
|
||||
.col(ColumnDef::new(Session::Expiry)
|
||||
.timestamp()
|
||||
.not_null()
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Session::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
pub enum Session {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Ip,
|
||||
Token,
|
||||
Context,
|
||||
Expiry,
|
||||
}
|
57
migration/src/m20220907_223633_create_teams.rs
Normal file
57
migration/src/m20220907_223633_create_teams.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20220907_223633_create_teams"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Team::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(Team::Id)
|
||||
.string()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
)
|
||||
.col(ColumnDef::new(Team::Name)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.col(ColumnDef::new(Team::Active)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true)
|
||||
)
|
||||
.col(ColumnDef::new(Team::Personal)
|
||||
.boolean()
|
||||
.not_null()
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Team::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
pub enum Team {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Active,
|
||||
Personal
|
||||
}
|
68
migration/src/m20220907_223634_create_team_members.rs
Normal file
68
migration/src/m20220907_223634_create_team_members.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
use crate::m20220907_223615_create_users::User;
|
||||
use crate::m20220907_223633_create_teams::Team;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20220907_223634_create_team_members"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(TeamMember::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(TeamMember::Id)
|
||||
.string()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
)
|
||||
.col(ColumnDef::new(TeamMember::TeamId)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.foreign_key(ForeignKey::create()
|
||||
.name("fk-team-members-team-id")
|
||||
.from(TeamMember::Table, TeamMember::TeamId)
|
||||
.to(Team::Table, Team::Id)
|
||||
)
|
||||
.col(ColumnDef::new(TeamMember::UserId)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.foreign_key(ForeignKey::create()
|
||||
.name("fk-team-members-user-id")
|
||||
.from(TeamMember::Table, TeamMember::UserId)
|
||||
.to(User::Table, User::Id)
|
||||
)
|
||||
.col(ColumnDef::new(TeamMember::Permission)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(TeamMember::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
enum TeamMember {
|
||||
Table,
|
||||
Id,
|
||||
TeamId,
|
||||
UserId,
|
||||
Permission
|
||||
}
|
65
migration/src/m20220907_223637_create_zones.rs
Normal file
65
migration/src/m20220907_223637_create_zones.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
use crate::m20220907_223633_create_teams::Team;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20220907_223637_create_zones"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Zone::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(Zone::Id)
|
||||
.string()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
)
|
||||
.col(ColumnDef::new(Zone::Owner)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.foreign_key(ForeignKey::create()
|
||||
.name("fk-zone-owner-id")
|
||||
.from(Zone::Table, Zone::Owner)
|
||||
.to(Team::Table, Team::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
)
|
||||
.col(ColumnDef::new(Zone::Origin)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key()
|
||||
)
|
||||
.col(ColumnDef::new(Zone::Delegated)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(false)
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Zone::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
pub enum Zone {
|
||||
Table,
|
||||
Id,
|
||||
Owner,
|
||||
Origin,
|
||||
Delegated
|
||||
}
|
64
migration/src/m20220907_223639_create_records.rs
Normal file
64
migration/src/m20220907_223639_create_records.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
use crate::m20220907_223637_create_zones::Zone;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20220907_223639_create_records"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Record::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(Record::Id)
|
||||
.string()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
)
|
||||
.col(ColumnDef::new(Record::Zone)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.foreign_key(ForeignKey::create()
|
||||
.name("fk-record-zone-id")
|
||||
.from(Record::Table, Record::Zone)
|
||||
.to(Zone::Table, Zone::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
)
|
||||
.col(ColumnDef::new(Record::Value)
|
||||
.binary()
|
||||
.not_null()
|
||||
)
|
||||
.col(ColumnDef::new(Record::Active)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true)
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Record::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
pub enum Record {
|
||||
Table,
|
||||
Id,
|
||||
Zone,
|
||||
Value,
|
||||
Active,
|
||||
}
|
65
migration/src/m20220907_223653_create_proxies.rs
Normal file
65
migration/src/m20220907_223653_create_proxies.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
use crate::m20220907_223639_create_records::Record;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20220907_223653_create_proxies"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Proxy::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(Proxy::Id)
|
||||
.string()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
)
|
||||
.col(ColumnDef::new(Proxy::Record)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.foreign_key(ForeignKey::create()
|
||||
.name("fk-proxy-record-id")
|
||||
.from(Proxy::Table, Proxy::Record)
|
||||
.to(Record::Table, Record::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
)
|
||||
.col(ColumnDef::new(Proxy::Port)
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(443)
|
||||
)
|
||||
.col(ColumnDef::new(Proxy::Active)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true)
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Proxy::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
enum Proxy {
|
||||
Table,
|
||||
Id,
|
||||
Record,
|
||||
Port,
|
||||
Active,
|
||||
}
|
81
migration/src/m20220908_204553_create_clients.rs
Normal file
81
migration/src/m20220908_204553_create_clients.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20220908_204553_create_clients"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Client::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(Client::Id)
|
||||
.string()
|
||||
.not_null()
|
||||
.primary_key()
|
||||
)
|
||||
.col(ColumnDef::new(Client::Name)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key()
|
||||
)
|
||||
.col(ColumnDef::new(Client::Ip)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.col(ColumnDef::new(Client::Key)
|
||||
.string()
|
||||
.not_null()
|
||||
)
|
||||
.col(ColumnDef::new(Client::Active)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true)
|
||||
)
|
||||
.col(ColumnDef::new(Client::DNS)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true)
|
||||
)
|
||||
.col(ColumnDef::new(Client::Proxy)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true)
|
||||
)
|
||||
.col(ColumnDef::new(Client::Health)
|
||||
.string()
|
||||
.not_null()
|
||||
.default("DEAD")
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Client::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
enum Client {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
Ip,
|
||||
Key,
|
||||
Active,
|
||||
DNS,
|
||||
Proxy,
|
||||
Health
|
||||
}
|
6
migration/src/main.rs
Normal file
6
migration/src/main.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
cli::run_cli(migration::Migrator).await;
|
||||
}
|
1
src/dns/mod.rs
Normal file
1
src/dns/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod records;
|
78
src/dns/records.rs
Normal file
78
src/dns/records.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum RecordTypes {
|
||||
SOA {
|
||||
/// Time-to-live
|
||||
ttl: i32,
|
||||
/// Primary master name server
|
||||
mname: String,
|
||||
/// Email address of the administrator
|
||||
rname: String,
|
||||
/// Serial number
|
||||
serial: u32,
|
||||
/// Seconds until secondary name server refresh
|
||||
refresh: i32,
|
||||
/// Seconds after initial failure to retry from secondary name servers
|
||||
retry: i32,
|
||||
/// Seconds until secondary name servers give up if repeated failures
|
||||
expire: i32,
|
||||
/// Minimum time-to-live for negative caching
|
||||
minimum: u32
|
||||
},
|
||||
A {
|
||||
hostname: String,
|
||||
ttl: i32,
|
||||
address: Ipv4Addr
|
||||
},
|
||||
AAAA {
|
||||
hostname: String,
|
||||
ttl: i32,
|
||||
address: Ipv6Addr
|
||||
},
|
||||
CNAME {
|
||||
hostname: String,
|
||||
ttl: i32,
|
||||
cname: String
|
||||
},
|
||||
DNAME {
|
||||
hostname: String,
|
||||
ttl: i32,
|
||||
dname: String
|
||||
},
|
||||
MX {
|
||||
hostname: String,
|
||||
ttl: i32,
|
||||
preference: i16,
|
||||
exchange: String
|
||||
},
|
||||
NS {
|
||||
hostname: String,
|
||||
ttl: i32,
|
||||
nsdame: String
|
||||
},
|
||||
PTR {
|
||||
hostname: String,
|
||||
ttl: i32,
|
||||
nsdame: String
|
||||
},
|
||||
TXT {
|
||||
hostname: String,
|
||||
ttl: i32,
|
||||
txt_data: String
|
||||
},
|
||||
CAA {
|
||||
hostname: String,
|
||||
ttl: i32,
|
||||
property: String
|
||||
},
|
||||
SRV {
|
||||
hostname: String,
|
||||
ttl: i32,
|
||||
priority: u16,
|
||||
weight: u16,
|
||||
port: u16,
|
||||
target: String
|
||||
}
|
||||
}
|
29
src/entities/client.rs
Normal file
29
src/entities/client.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "client")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
#[sea_orm(unique)]
|
||||
pub name: String,
|
||||
pub ip: String,
|
||||
pub key: String,
|
||||
pub active: bool,
|
||||
pub dns: bool,
|
||||
pub proxy: bool,
|
||||
pub health: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl RelationTrait for Relation {
|
||||
fn def(&self) -> RelationDef {
|
||||
panic!("No RelationDef")
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
23
src/entities/controller.rs
Normal file
23
src/entities/controller.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "controller")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub active: bool,
|
||||
pub health: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl RelationTrait for Relation {
|
||||
fn def(&self) -> RelationDef {
|
||||
panic!("No RelationDef")
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
12
src/entities/mod.rs
Normal file
12
src/entities/mod.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod client;
|
||||
pub mod proxy;
|
||||
pub mod record;
|
||||
pub mod session;
|
||||
pub mod team;
|
||||
pub mod team_member;
|
||||
pub mod user;
|
||||
pub mod zone;
|
10
src/entities/prelude.rs
Normal file
10
src/entities/prelude.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
|
||||
|
||||
pub use super::client::Entity as Client;
|
||||
pub use super::proxy::Entity as Proxy;
|
||||
pub use super::record::Entity as Record;
|
||||
pub use super::session::Entity as Session;
|
||||
pub use super::team::Entity as Team;
|
||||
pub use super::team_member::Entity as TeamMember;
|
||||
pub use super::user::Entity as User;
|
||||
pub use super::zone::Entity as Zone;
|
33
src/entities/proxy.rs
Normal file
33
src/entities/proxy.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "proxy")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub record: String,
|
||||
pub port: i32,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::record::Entity",
|
||||
from = "Column::Record",
|
||||
to = "super::record::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Record,
|
||||
}
|
||||
|
||||
impl Related<super::record::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Record.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
41
src/entities/record.rs
Normal file
41
src/entities/record.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "record")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub zone: String,
|
||||
pub value: Vec<u8>,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::zone::Entity",
|
||||
from = "Column::Zone",
|
||||
to = "super::zone::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Zone,
|
||||
#[sea_orm(has_many = "super::proxy::Entity")]
|
||||
Proxy,
|
||||
}
|
||||
|
||||
impl Related<super::zone::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Zone.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::proxy::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Proxy.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
35
src/entities/session.rs
Normal file
35
src/entities/session.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "session")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub ip: String,
|
||||
pub token: String,
|
||||
pub context: String,
|
||||
pub expiry: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::Context",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
35
src/entities/team.rs
Normal file
35
src/entities/team.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "team")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub active: bool,
|
||||
pub personal: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::team_member::Entity")]
|
||||
TeamMember,
|
||||
#[sea_orm(has_many = "super::zone::Entity")]
|
||||
Zone,
|
||||
}
|
||||
|
||||
impl Related<super::team_member::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::TeamMember.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::zone::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Zone.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
47
src/entities/team_member.rs
Normal file
47
src/entities/team_member.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "team_member")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub team_id: String,
|
||||
pub user_id: String,
|
||||
pub permission: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::team::Entity",
|
||||
from = "Column::TeamId",
|
||||
to = "super::team::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
)]
|
||||
Team,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::team::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Team.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
38
src/entities/user.rs
Normal file
38
src/entities/user.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "user")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[sea_orm(unique)]
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub active: bool,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::session::Entity")]
|
||||
Session,
|
||||
#[sea_orm(has_many = "super::team_member::Entity")]
|
||||
TeamMember,
|
||||
}
|
||||
|
||||
impl Related<super::session::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Session.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::team_member::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::TeamMember.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
42
src/entities/zone.rs
Normal file
42
src/entities/zone.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "zone")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub owner: String,
|
||||
#[sea_orm(unique)]
|
||||
pub origin: String,
|
||||
pub delegated: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::team::Entity",
|
||||
from = "Column::Owner",
|
||||
to = "super::team::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Team,
|
||||
#[sea_orm(has_many = "super::record::Entity")]
|
||||
Record,
|
||||
}
|
||||
|
||||
impl Related<super::team::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Team.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::record::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Record.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
116
src/main.rs
Normal file
116
src/main.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
#[macro_use] extern crate log;
|
||||
|
||||
use std::env;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
|
||||
use axum::extract::Extension;
|
||||
use axum::Router;
|
||||
use axum::routing::{delete, get, post};
|
||||
use dotenv::dotenv;
|
||||
use sea_orm::{ConnectOptions, Database};
|
||||
use sea_orm_migration::prelude::*;
|
||||
use tower::ServiceBuilder;
|
||||
|
||||
mod entities;
|
||||
mod dns;
|
||||
mod util;
|
||||
mod routes;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
|
||||
// Default to logging all info logs
|
||||
if env::var("RUST_LOG").is_err() {
|
||||
env::set_var("RUST_LOG", "info")
|
||||
}
|
||||
|
||||
pretty_env_logger::init();
|
||||
|
||||
info!("██████╗ ██████╗ ██╗██████╗ ████████╗ ██████╗ ██████╗ ██████╗██╗ ██╗");
|
||||
info!("██╔══██╗██╔══██╗██║██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝██║ ██║");
|
||||
info!("██║ ██║██████╔╝██║██████╔╝ ██║ ██║ ██║██████╔╝██║ ███████║");
|
||||
info!("██║ ██║██╔══██╗██║██╔═══╝ ██║ ██║ ██║██╔══██╗██║ ██╔══██║");
|
||||
info!("██████╔╝██║ ██║██║██║ ██║ ╚██████╔╝██║ ██║╚██████╗██║ ██║");
|
||||
info!("╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝");
|
||||
|
||||
info!("._______ ._______ .______ _____._.______ ._______ .___ .___ ._______.______");
|
||||
info!(":_. ___\\: .___ \\ : \\ \\__ _:|: __ \\ : .___ \\ | | | | : .____/: __ \\ ");
|
||||
info!("| : |/\\ | : | || | | :|| \\____|| : | || | | | | : _/\\ | \\____|");
|
||||
info!("| / \\| : || | | | || : \\ | : || |/\\ | |/\\ | / \\| : \\ ");
|
||||
info!("|. _____/ \\_. ___/ |___| | | || |___\\ \\_. ___/ | / \\| / \\|_.: __/| |___\\");
|
||||
info!(" :/ :/ |___| |___||___| :/ |______/|______/ :/ |___| ");
|
||||
info!(" : : : ");
|
||||
|
||||
info!("Version {}", VERSION);
|
||||
|
||||
info!("Checking for supplemental files...");
|
||||
if !Path::new(&env::var("UAP_REGEXES").unwrap_or(String::from("./regexes.yaml"))).exists(){
|
||||
error!("Please download https://github.com/ua-parser/uap-core/blob/master/regexes.yaml either place it next to the executable or add it's path to env variable UAP_REGEXES! Halting start-up.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
info!("Connecting to database...");
|
||||
let database_url = env::var("DATABASE_URL")
|
||||
.expect("DATABASE_URL must be set! Halting start-up.");
|
||||
let connection = Database::connect(
|
||||
ConnectOptions::new(database_url).sqlx_logging(false).to_owned())
|
||||
.await
|
||||
.expect("Failed to connect to the database! Halting start-up.");
|
||||
|
||||
info!("Running migrations...");
|
||||
migration::Migrator::up(&connection, None)
|
||||
.await
|
||||
.expect("Failed to run migrations! Halting start-up.");
|
||||
|
||||
info!("Starting web server...");
|
||||
let app = Router::new()
|
||||
.route("/", get(routes::status::status))
|
||||
|
||||
// Auth
|
||||
.route("/user/register", post(routes::auth::register::register))
|
||||
.route("/user/login", post(routes::auth::login::login))
|
||||
.route("/user/logout", post(routes::auth::logout::logout))
|
||||
.route("/user/delete", delete(routes::auth::delete::delete))
|
||||
.route("/user/list_sessions", get(routes::auth::list_sessions::list_sessions))
|
||||
|
||||
// Teams
|
||||
|
||||
// Zones
|
||||
|
||||
// Records
|
||||
|
||||
// Proxies
|
||||
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(Extension(connection))
|
||||
);
|
||||
|
||||
let addr = env::var("LISTEN_ADDR")
|
||||
.unwrap_or("127.0.0.1:32204".to_string());
|
||||
|
||||
let socket_addr: SocketAddr = addr.parse().expect("Failed to parse LISTEN_ADDR! Halting start-up.");
|
||||
|
||||
let axum_builder = axum::Server::try_bind(&socket_addr);
|
||||
|
||||
match axum_builder {
|
||||
Ok(_) => {
|
||||
info!("Driptorch Controller v{} is now listening on {}!", VERSION, socket_addr);
|
||||
|
||||
axum_builder
|
||||
.expect("Passed builder match but still returned error? Halting start-up.")
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await
|
||||
.expect("Failed to bind to port! Halting start-up.");
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Driptorch Controller v{} failed to bind to {}! Halting start-up.", VERSION, socket_addr);
|
||||
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
81
src/routes/auth/delete.rs
Normal file
81
src/routes/auth/delete.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
use axum::Extension;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||
use crate::entities::prelude::{Team, TeamMember};
|
||||
use crate::entities::{team, team_member, user};
|
||||
use crate::util::auth::{TeamPermissions, UserFromBearer};
|
||||
|
||||
pub async fn delete(
|
||||
Extension(ref connection): Extension<DatabaseConnection>,
|
||||
UserFromBearer(user): UserFromBearer,
|
||||
) -> impl IntoResponse {
|
||||
let user = user.0;
|
||||
|
||||
let owned_teams = TeamMember::find()
|
||||
.filter(team_member::Column::UserId.eq(user.clone().id))
|
||||
.filter(team_member::Column::Permission.eq(TeamPermissions::OWNER.to_string()))
|
||||
.all(connection)
|
||||
.await
|
||||
.expect("Failed to retrieve teams from database");
|
||||
|
||||
let mut personal_team: Option<team::Model> = None;
|
||||
|
||||
for team in owned_teams {
|
||||
let owned_team = Team::find_by_id(team.clone().team_id)
|
||||
.one(connection)
|
||||
.await
|
||||
.expect("Failed to get owned team by id");
|
||||
|
||||
match owned_team {
|
||||
None => {
|
||||
error!("team_member still exists for {} for {} but the team doesn't exist!", team.team_id, user.id);
|
||||
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "An internal server error has occurred".to_string());
|
||||
}
|
||||
Some(owned_team) => {
|
||||
if !owned_team.personal {
|
||||
return (StatusCode::BAD_REQUEST, "User owns non-personal teams.".to_string());
|
||||
} else {
|
||||
personal_team = Some(owned_team.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let personal_team = personal_team.unwrap();
|
||||
|
||||
// Kick everyone out of the user's personal team
|
||||
let team_member_delete = team_member::Entity::delete_many()
|
||||
.filter(team_member::Column::TeamId.eq(personal_team.clone().id))
|
||||
.exec(connection)
|
||||
.await
|
||||
.expect("Could not delete team_member during user deletion!");
|
||||
|
||||
if team_member_delete.rows_affected.eq(&0) {
|
||||
error!("Could not delete personal team_member for {}!", user.id);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "An internal server error has occurred".to_string());
|
||||
}
|
||||
|
||||
let team_delete = team::Entity::delete_by_id(personal_team.clone().id)
|
||||
.exec(connection)
|
||||
.await
|
||||
.expect("Failed to delete personal team during user deletion!");
|
||||
|
||||
if team_delete.rows_affected.eq(&0) {
|
||||
error!("Could not delete personal team for {}!", user.id);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "An internal server error has occurred".to_string());
|
||||
}
|
||||
|
||||
let user_delete = user::Entity::delete_by_id(user.clone().id)
|
||||
.exec(connection)
|
||||
.await
|
||||
.expect("Failed to delete user!");
|
||||
|
||||
if user_delete.rows_affected.eq(&0) {
|
||||
error!("Could not delete user {}!", user.id);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "An internal server error has occurred".to_string());
|
||||
}
|
||||
|
||||
(StatusCode::OK, format!("Goodbye forever, {}!", user.name))
|
||||
}
|
45
src/routes/auth/list_sessions.rs
Normal file
45
src/routes/auth/list_sessions.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use axum::{Extension, Json};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use sea_orm::{DatabaseConnection, EntityTrait};
|
||||
use crate::entities::prelude::Session;
|
||||
use crate::entities::session;
|
||||
use crate::util::auth::UserFromBearer;
|
||||
use sea_orm::{QueryFilter, ColumnTrait};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ListSessionsResponse {
|
||||
sessions: Vec<ListSession>
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ListSession {
|
||||
id: String,
|
||||
name: String,
|
||||
ip: String
|
||||
}
|
||||
|
||||
pub async fn list_sessions(
|
||||
Extension(ref connection): Extension<DatabaseConnection>,
|
||||
UserFromBearer(user): UserFromBearer,
|
||||
) -> impl IntoResponse {
|
||||
let user = user.0;
|
||||
let mut listed_sessions: Vec<ListSession> = Vec::new();
|
||||
|
||||
let total_sessions: Vec<session::Model> = Session::find()
|
||||
.filter(session::Column::Context.eq(user.clone().id))
|
||||
.all(connection)
|
||||
.await
|
||||
.expect("Failed to access database");
|
||||
|
||||
for session in total_sessions {
|
||||
listed_sessions.push(ListSession {
|
||||
id: session.id,
|
||||
name: session.name,
|
||||
ip: session.ip
|
||||
});
|
||||
}
|
||||
|
||||
(StatusCode::OK, Json(ListSessionsResponse { sessions: listed_sessions }))
|
||||
}
|
153
src/routes/auth/login.rs
Normal file
153
src/routes/auth/login.rs
Normal file
|
@ -0,0 +1,153 @@
|
|||
use std::net::SocketAddr;
|
||||
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
||||
use axum::{Extension, Form, Json};
|
||||
use axum::extract::ConnectInfo;
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use chrono::{Duration, NaiveDateTime};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::entities::session;
|
||||
use crate::entities::session::Entity as Session;
|
||||
use crate::entities::user;
|
||||
use crate::entities::user::Entity as User;
|
||||
use crate::util::auth::assemble_session_name;
|
||||
use crate::util::generate_session_token;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AuthUserForm {
|
||||
email: String,
|
||||
password: String
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AuthUserFormIssues {
|
||||
email: Vec<String>,
|
||||
password: Vec<String>
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AuthUserFormResponse {
|
||||
session_token: Option<String>,
|
||||
issues: Option<AuthUserFormIssues>
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
Extension(ref connection): Extension<DatabaseConnection>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
headers: HeaderMap,
|
||||
Form(input): Form<AuthUserForm>
|
||||
) -> impl IntoResponse {
|
||||
let mut validation_issues = AuthUserFormIssues {
|
||||
email: vec![],
|
||||
password: vec![]
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref EMAIL_RE: Regex = Regex::new(r#"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#)
|
||||
.expect("Failed to compile email regex");
|
||||
}
|
||||
|
||||
// Ensure form contents are safe
|
||||
if input.email.is_empty() {
|
||||
validation_issues.email.push("Email cannot be empty.".to_string());
|
||||
} else if !EMAIL_RE.is_match(&input.email) {
|
||||
validation_issues.email.push("Email is invalid.".to_string());
|
||||
}
|
||||
|
||||
if input.password.is_empty(){
|
||||
validation_issues.password.push("Password cannot be empty.".to_string());
|
||||
}
|
||||
|
||||
// Return early if we have issues with form content so far
|
||||
if !validation_issues.email.is_empty() || !validation_issues.password.is_empty() {
|
||||
return (StatusCode::BAD_REQUEST, Json(AuthUserFormResponse { session_token: None, issues: Some(validation_issues) }));
|
||||
}
|
||||
|
||||
// Check to see if a user with the email exists
|
||||
let existing_user: Option<user::Model> = User::find()
|
||||
.filter(user::Column::Email.eq(input.email.clone()))
|
||||
.one(connection)
|
||||
.await
|
||||
.expect("Failed to check database.");
|
||||
|
||||
if !existing_user.is_some() {
|
||||
validation_issues.email.push("An account with this email doesn't exist.".to_string());
|
||||
|
||||
return (StatusCode::BAD_REQUEST, Json(AuthUserFormResponse { session_token: None, issues: Some(validation_issues) }));
|
||||
}
|
||||
|
||||
let existing_user = existing_user.unwrap();
|
||||
|
||||
// Check password
|
||||
let existing_password_hash = PasswordHash::new(&existing_user.password)
|
||||
.expect("Failed to generate password hash from database.");
|
||||
if !Argon2::default().verify_password(input.password.as_bytes(), &existing_password_hash).is_ok() {
|
||||
validation_issues.password.push("Incorrect password.".to_string());
|
||||
return (StatusCode::BAD_REQUEST, Json(AuthUserFormResponse { session_token: None, issues: Some(validation_issues) }));
|
||||
}
|
||||
|
||||
let session_name;
|
||||
match headers.get("User-Agent") {
|
||||
None => {
|
||||
session_name = String::from("Unknown");
|
||||
}
|
||||
Some(header) => {
|
||||
match header.to_str() {
|
||||
Ok(header) => {
|
||||
session_name = assemble_session_name(header).await;
|
||||
}
|
||||
Err(_) => {
|
||||
session_name = String::from("Unknown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ip;
|
||||
match headers.get("X-Real-IP") {
|
||||
None => {
|
||||
ip = addr.ip().to_string();
|
||||
}
|
||||
Some(header) => {
|
||||
match header.to_str() {
|
||||
Ok(header) => {
|
||||
ip = String::from(header);
|
||||
}
|
||||
Err(_) => {
|
||||
ip = addr.ip().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let session_token = generate_session_token();
|
||||
let expiry: NaiveDateTime = chrono::offset::Utc::now().naive_local() + Duration::days(20);
|
||||
|
||||
// Generate session for newly created user
|
||||
let new_session = session::ActiveModel {
|
||||
id: ActiveValue::Set(Ulid::new().to_string()),
|
||||
name: ActiveValue::Set(session_name.clone()),
|
||||
ip: ActiveValue::set(ip.clone()),
|
||||
token: ActiveValue::Set(session_token.clone()),
|
||||
context: ActiveValue::Set(existing_user.id.clone()),
|
||||
expiry: ActiveValue::Set(expiry)
|
||||
};
|
||||
|
||||
let session_res = Session::insert(new_session)
|
||||
.exec(connection)
|
||||
.await;
|
||||
|
||||
match session_res {
|
||||
Ok(_) => {
|
||||
(StatusCode::OK, Json(AuthUserFormResponse { session_token: Some(session_token), issues: None }))
|
||||
}
|
||||
Err(_) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(AuthUserFormResponse { session_token: None, issues: None }))
|
||||
}
|
||||
}
|
||||
}
|
100
src/routes/auth/logout.rs
Normal file
100
src/routes/auth/logout.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use axum::{Extension, extract};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||
use crate::util::auth::UserFromBearer;
|
||||
use serde::Deserialize;
|
||||
use crate::entities::prelude::Session;
|
||||
use crate::entities::session;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LogoutInput {
|
||||
session_id: String
|
||||
}
|
||||
|
||||
pub async fn logout(
|
||||
Extension(ref connection): Extension<DatabaseConnection>,
|
||||
UserFromBearer(user): UserFromBearer,
|
||||
extract::Json(payload): extract::Json<LogoutInput>
|
||||
) -> impl IntoResponse {
|
||||
let accessed_session_id = user.1;
|
||||
let user = user.0;
|
||||
|
||||
return match payload.session_id.as_str() {
|
||||
"ALL" => {
|
||||
// Invalidate all of the user's sessions
|
||||
// By connecting to this route, we know there is at least 1 session
|
||||
session::Entity::delete_many()
|
||||
.filter(session::Column::Context.eq(user.clone().id))
|
||||
.exec(connection)
|
||||
.await
|
||||
.expect("Failed to run delete on sessions database");
|
||||
|
||||
// Ensure no sessions are left for the user
|
||||
let total_sessions = Session::find()
|
||||
.filter(session::Column::Context.eq(user.clone().id))
|
||||
.all(connection)
|
||||
.await
|
||||
.expect("Failed to access database");
|
||||
|
||||
if total_sessions.is_empty() {
|
||||
(StatusCode::OK, format!("Goodbye {}!", user.name))
|
||||
} else {
|
||||
error!("Unable to delete {}'s sessions!", user.id);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "An Internal Server Error has occurred".to_string())
|
||||
}
|
||||
}
|
||||
"" => {
|
||||
// Invalidate the user's current session
|
||||
let session_deletion = session::Entity::delete_by_id(accessed_session_id.clone())
|
||||
.exec(connection)
|
||||
.await
|
||||
.expect("Failed to run delete on sessions database");
|
||||
|
||||
if session_deletion.rows_affected.eq(&1) {
|
||||
(StatusCode::OK, format!("Goodbye {}!", user.name))
|
||||
} else {
|
||||
error!("Unable to delete {}'s current session! {}", user.id, accessed_session_id);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "An Internal Server Error has occurred".to_string())
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Check if the ID is valid and exists
|
||||
let requested_session = Session::find()
|
||||
.filter(session::Column::Id.eq(payload.session_id.clone()))
|
||||
.one(connection)
|
||||
.await;
|
||||
|
||||
match requested_session {
|
||||
Ok(_) => {
|
||||
let requested_session = requested_session.unwrap();
|
||||
if requested_session.is_some() {
|
||||
let requested_session = requested_session.unwrap();
|
||||
// Ensure the session is for the currently authed user
|
||||
if requested_session.context != user.id {
|
||||
return (StatusCode::BAD_REQUEST, "Requested session ID is invalid".to_string());
|
||||
}
|
||||
|
||||
// Invalidate the session
|
||||
let session_deletion = session::Entity::delete_by_id(requested_session.clone().id)
|
||||
.exec(connection)
|
||||
.await
|
||||
.expect("Failed to run delete on sessions database");
|
||||
|
||||
if session_deletion.rows_affected.eq(&1) {
|
||||
(StatusCode::OK, format!("Goodbye {}!", user.name))
|
||||
} else {
|
||||
error!("Unable to delete {}'s requested session! {}", user.id, requested_session.clone().id);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "An Internal Server Error has occurred".to_string())
|
||||
}
|
||||
} else {
|
||||
(StatusCode::BAD_REQUEST, "Requested session ID is invalid".to_string())
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
(StatusCode::BAD_REQUEST, "Requested session ID is invalid".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
src/routes/auth/mod.rs
Normal file
5
src/routes/auth/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod register;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod list_sessions;
|
||||
pub mod delete;
|
230
src/routes/auth/register.rs
Normal file
230
src/routes/auth/register.rs
Normal file
|
@ -0,0 +1,230 @@
|
|||
use std::net::SocketAddr;
|
||||
use axum::{Extension, Form, Json};
|
||||
use axum::extract::ConnectInfo;
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use chrono::{Duration, NaiveDateTime};
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::entities::{team, team_member, user};
|
||||
use crate::entities::prelude::{Team, TeamMember};
|
||||
use crate::entities::user::Entity as User;
|
||||
|
||||
use crate::entities::session;
|
||||
use crate::entities::session::Entity as Session;
|
||||
|
||||
use crate::util::{generate_session_token, hash_password};
|
||||
use crate::util::auth::{assemble_session_name, TeamPermissions};
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct NewUserForm {
|
||||
name: String,
|
||||
email: String,
|
||||
password: String
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NewUserFormIssues {
|
||||
name: Vec<String>,
|
||||
email: Vec<String>,
|
||||
password: Vec<String>
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NewUserFormResponse {
|
||||
session_token: Option<String>,
|
||||
issues: Option<NewUserFormIssues>
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
Extension(ref connection): Extension<DatabaseConnection>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
headers: HeaderMap,
|
||||
Form(input): Form<NewUserForm>
|
||||
) -> impl IntoResponse {
|
||||
let mut validation_issues = NewUserFormIssues {
|
||||
name: vec![],
|
||||
email: vec![],
|
||||
password: vec![]
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref EMAIL_RE: Regex = Regex::new(r#"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#)
|
||||
.expect("Failed to compile email regex");
|
||||
}
|
||||
|
||||
// Ensure form contents are safe
|
||||
if input.name.is_empty() {
|
||||
validation_issues.name.push("Name cannot be empty.".to_string());
|
||||
} else if input.name.len() > 128 {
|
||||
validation_issues.name.push("Name is too long.".to_string());
|
||||
}
|
||||
|
||||
if input.email.is_empty() {
|
||||
validation_issues.email.push("Email cannot be empty.".to_string());
|
||||
} else if !EMAIL_RE.is_match(&input.email) {
|
||||
validation_issues.email.push("Email is invalid.".to_string());
|
||||
}
|
||||
|
||||
if input.password.is_empty(){
|
||||
validation_issues.password.push("Password cannot be empty.".to_string());
|
||||
} else {
|
||||
// Check password security
|
||||
let pass_estimate = zxcvbn::zxcvbn(&input.password, &[])
|
||||
.expect("Failed to check password security.");
|
||||
|
||||
if pass_estimate.score() <= 2 {
|
||||
for i in pass_estimate.feedback().clone().unwrap().suggestions() {
|
||||
validation_issues.password.push(i.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return early if we have issues with form content so far
|
||||
if !validation_issues.name.is_empty() || !validation_issues.email.is_empty() || !validation_issues.password.is_empty() {
|
||||
return (StatusCode::BAD_REQUEST, Json(NewUserFormResponse { session_token: None, issues: Some(validation_issues) }));
|
||||
}
|
||||
|
||||
// Check to see if a user with the email already exists
|
||||
let existing_user = User::find()
|
||||
.filter(user::Column::Email.eq(input.email.clone()))
|
||||
.one(connection)
|
||||
.await
|
||||
.expect("Failed to check database.");
|
||||
|
||||
if existing_user.is_some() {
|
||||
validation_issues.email.push("An account with this email already exists.".to_string());
|
||||
|
||||
return (StatusCode::BAD_REQUEST, Json(NewUserFormResponse { session_token: None, issues: Some(validation_issues) }));
|
||||
}
|
||||
|
||||
let user_id = Ulid::new().to_string();
|
||||
|
||||
let new_user = user::ActiveModel {
|
||||
id: ActiveValue::Set(user_id.clone()),
|
||||
name: ActiveValue::Set(input.clone().name),
|
||||
email: ActiveValue::Set(input.clone().email),
|
||||
password: ActiveValue::Set(hash_password(input.clone().password)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let res = User::insert(new_user.clone())
|
||||
.exec(connection)
|
||||
.await;
|
||||
|
||||
return match res {
|
||||
Ok(_) => {
|
||||
// Create a personal team
|
||||
let team_id = String::from(Ulid::new());
|
||||
|
||||
let new_team = team::ActiveModel {
|
||||
id: ActiveValue::set(team_id.clone()),
|
||||
name: ActiveValue::Set(
|
||||
format!("{}'s Personal Team", &input.name)
|
||||
),
|
||||
active: Default::default(),
|
||||
personal: ActiveValue::Set(true)
|
||||
};
|
||||
|
||||
let team_creation = Team::insert(new_team.clone())
|
||||
.exec(connection)
|
||||
.await;
|
||||
|
||||
if team_creation.is_err() {
|
||||
// Delete user on team creation error
|
||||
new_user.delete(connection).await
|
||||
.expect("Failed to delete user from database after failing to create personal team for said user!");
|
||||
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None }));
|
||||
}
|
||||
|
||||
// Add user to personal team
|
||||
let team_addition = TeamMember::insert(
|
||||
team_member::ActiveModel {
|
||||
id: ActiveValue::Set(String::from(Ulid::new())),
|
||||
team_id: ActiveValue::Set(team_id.clone()),
|
||||
user_id: ActiveValue::Set(user_id.clone()),
|
||||
permission: ActiveValue::Set(TeamPermissions::OWNER.to_string())
|
||||
}
|
||||
).exec(connection)
|
||||
.await;
|
||||
|
||||
if team_addition.is_err() {
|
||||
// Delete team and user on permission addition error
|
||||
new_team.delete(connection).await
|
||||
.expect("Failed to delete team from database after failing to add permissions for personal team for said user!");
|
||||
new_user.delete(connection).await
|
||||
.expect("Failed to delete user from database after failing to add permissions for personal team for said user!");
|
||||
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None }));
|
||||
}
|
||||
|
||||
let session_name;
|
||||
match headers.get("User-Agent") {
|
||||
None => {
|
||||
session_name = String::from("Unknown");
|
||||
}
|
||||
Some(header) => {
|
||||
match header.to_str() {
|
||||
Ok(header) => {
|
||||
session_name = assemble_session_name(header).await;
|
||||
}
|
||||
Err(_) => {
|
||||
session_name = String::from("Unknown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ip;
|
||||
match headers.get("X-Real-IP") {
|
||||
None => {
|
||||
ip = addr.ip().to_string();
|
||||
}
|
||||
Some(header) => {
|
||||
match header.to_str() {
|
||||
Ok(header) => {
|
||||
ip = String::from(header);
|
||||
}
|
||||
Err(_) => {
|
||||
ip = addr.ip().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let session_token = generate_session_token();
|
||||
let expiry: NaiveDateTime = chrono::offset::Utc::now().naive_local() + Duration::days(20);
|
||||
|
||||
// Generate session for newly created user
|
||||
let new_session = session::ActiveModel {
|
||||
id: ActiveValue::Set(Ulid::new().to_string()),
|
||||
name: ActiveValue::Set(session_name.clone()),
|
||||
ip: ActiveValue::set(ip.clone()),
|
||||
token: ActiveValue::Set(session_token.clone()),
|
||||
context: ActiveValue::Set(user_id.clone()),
|
||||
expiry: ActiveValue::Set(expiry)
|
||||
};
|
||||
|
||||
let session_res = Session::insert(new_session)
|
||||
.exec(connection)
|
||||
.await;
|
||||
|
||||
match session_res {
|
||||
Ok(_) => {
|
||||
(StatusCode::CREATED, Json(NewUserFormResponse { session_token: Some(session_token), issues: None }))
|
||||
}
|
||||
Err(_) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None }))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None }))
|
||||
}
|
||||
}
|
||||
}
|
2
src/routes/mod.rs
Normal file
2
src/routes/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod status;
|
||||
pub mod auth;
|
34
src/routes/status.rs
Normal file
34
src/routes/status.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use axum::http::StatusCode;
|
||||
use axum::Json;
|
||||
use axum::response::IntoResponse;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::VERSION;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum Health {
|
||||
HEALTHY,
|
||||
UNHEALTHY,
|
||||
DEAD
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum Context {
|
||||
CONTROLLER,
|
||||
CLIENT
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Status {
|
||||
pub context: Context,
|
||||
pub version: String,
|
||||
pub health: Health
|
||||
}
|
||||
|
||||
pub async fn status() -> impl IntoResponse {
|
||||
(StatusCode::OK, Json(Status {
|
||||
context: Context::CONTROLLER,
|
||||
version: VERSION.to_string(),
|
||||
health: Health::HEALTHY
|
||||
})
|
||||
)
|
||||
}
|
156
src/util/auth.rs
Normal file
156
src/util/auth.rs
Normal file
|
@ -0,0 +1,156 @@
|
|||
use std::borrow::Cow;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::http::{header::AUTHORIZATION, StatusCode};
|
||||
use axum::http::request::Parts;
|
||||
use lazy_static::lazy_static;
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||
use user_agent_parser::{OS, Product};
|
||||
use user_agent_parser::UserAgentParser;
|
||||
|
||||
use crate::entities::{session, user};
|
||||
use crate::entities::prelude::{Session, User};
|
||||
|
||||
pub enum TeamPermissions {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
EDITOR,
|
||||
VIEWER
|
||||
}
|
||||
|
||||
impl fmt::Display for TeamPermissions {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
TeamPermissions::OWNER => write!(f, "OWNER"),
|
||||
TeamPermissions::ADMIN => write!(f, "ADMIN"),
|
||||
TeamPermissions::EDITOR => write!(f, "EDITOR"),
|
||||
TeamPermissions::VIEWER => write!(f, "VIEWER")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a user model and session id from a supplied session token
|
||||
pub async fn get_user_from_token(token: String, connection: &DatabaseConnection) -> Option<(user::Model, String)> {
|
||||
let requested_session: Option<session::Model> = Session::find()
|
||||
.filter(session::Column::Token.eq(token))
|
||||
.one(connection)
|
||||
.await
|
||||
.expect("Failed to retrieve session from the database.");
|
||||
|
||||
return match requested_session {
|
||||
None => {
|
||||
None
|
||||
}
|
||||
Some(_) => {
|
||||
let requested_session = requested_session.unwrap();
|
||||
|
||||
let contexted_user: Option<user::Model> = User::find()
|
||||
.filter(user::Column::Id.eq(requested_session.clone().context))
|
||||
.one(connection)
|
||||
.await
|
||||
.expect("Failed to retrieve user from the database.");
|
||||
|
||||
match contexted_user {
|
||||
None => {
|
||||
error!("Session {} still exists for user {} of which doesn't exist!", requested_session.id, requested_session.context);
|
||||
None
|
||||
}
|
||||
Some(_) => {
|
||||
Some((contexted_user.unwrap(), requested_session.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserFromBearer(pub (user::Model, String));
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for UserFromBearer
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
// Get authorisation header
|
||||
let authorisation = parts
|
||||
.headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or((StatusCode::UNAUTHORIZED, "`Authorization` header is missing"))?
|
||||
.to_str()
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"`Authorization` header contains invalid characters",
|
||||
)
|
||||
})?;
|
||||
|
||||
// Check that its a well-formed bearer and return
|
||||
let split = authorisation.split_once(' ');
|
||||
match split {
|
||||
Some((name, contents)) if name == "Bearer" => {
|
||||
// Get database connection from header
|
||||
let connection: &DatabaseConnection = parts.extensions.get::<DatabaseConnection>()
|
||||
.expect("Failed to get database connection from auth extractor");
|
||||
|
||||
match get_user_from_token(contents.to_string(), connection).await {
|
||||
None => {
|
||||
Err((StatusCode::UNAUTHORIZED, "Provided token is invalid"))
|
||||
}
|
||||
Some(user) => Ok(Self(user))
|
||||
}
|
||||
},
|
||||
_ => Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"`Authorization` header must be a bearer token",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn assemble_session_name(header: &str) -> String {
|
||||
// TODO: Maybe reconsider having a static session name?
|
||||
|
||||
lazy_static! {
|
||||
static ref UAP: UserAgentParser = UserAgentParser::from_path(&env::var("UAP_REGEXES").unwrap_or(String::from("./regexes.yaml"))).expect("Failed to load regexes.yaml");
|
||||
}
|
||||
|
||||
let product: Product = UAP.parse_product(&header);
|
||||
let os: OS = UAP.parse_os(&header);
|
||||
|
||||
let mut session_name_builder: String = String::new();
|
||||
|
||||
session_name_builder.push_str(
|
||||
&product.name.unwrap_or(Cow::from("Unknown"))
|
||||
);
|
||||
|
||||
if product.major.is_some() {
|
||||
session_name_builder.push_str(" ");
|
||||
|
||||
session_name_builder.push_str(
|
||||
&product.major.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
session_name_builder.push_str(" / ");
|
||||
|
||||
session_name_builder.push_str(
|
||||
&os.name.unwrap_or(Cow::from("Unknown"))
|
||||
);
|
||||
|
||||
if os.major.is_some() {
|
||||
session_name_builder.push_str(" ");
|
||||
|
||||
session_name_builder.push_str(
|
||||
&os.major.unwrap()
|
||||
)
|
||||
};
|
||||
|
||||
return session_name_builder
|
||||
}
|
25
src/util/mod.rs
Normal file
25
src/util/mod.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
pub mod auth;
|
||||
|
||||
use argon2::{Argon2, PasswordHasher};
|
||||
use argon2::password_hash::SaltString;
|
||||
use base64ct::{Base64UrlUnpadded, Encoding};
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
|
||||
pub fn generate_session_token() -> String {
|
||||
let mut randombytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut randombytes);
|
||||
|
||||
let session_token = Base64UrlUnpadded::encode_string(blake3::hash(&randombytes).as_bytes());
|
||||
|
||||
session_token
|
||||
}
|
||||
|
||||
pub fn hash_password(password: String) -> String {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
argon2.hash_password(password.as_bytes(), &salt)
|
||||
.expect("Failed to hash password!")
|
||||
.to_string()
|
||||
}
|
Loading…
Add table
Reference in a new issue