initial commit: build infrastructure and the /backup/preset [POST] endpoint

This commit is contained in:
antifallobst 2023-11-11 15:36:13 +01:00
commit e8043cfbd5
Signed by: antifallobst
GPG Key ID: 2B4F402172791BAF
13 changed files with 2624 additions and 0 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL="sqlite:store.db"

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea
/target
store.db

2305
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "admin-worker"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1.29", features = ["macros", "rt-multi-thread", "sync"] }
anyhow = "1.0"
serde = { version = "1.0.183", features = ["derive"] }
serde_json = "1.0.108"
env_logger = "0.10"
log = "0.4"
sqlx = { version = "0.7.1", features = ["runtime-tokio", "sqlite", "chrono"] }
actix-web = "4"
actix-web-httpauth = "0.8.0"

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Nerdcult Admin Worker
This is a helper program, which provides a not public accessible API for managing admin tasks.
It has full access to the host system.

36
src/api/calls.rs Normal file
View File

@ -0,0 +1,36 @@
use crate::api::{data::*, handlers, State};
use actix_web::{post, web, HttpResponse, Responder};
use log::error;
// #[post("/backup/create")]
// async fn backup_create(
// data: web::Data<State>,
// body: web::Json<BackupCreateRequest>,
// ) -> impl Responder {
// match handlers::backup_create(&data.pool, body.into_inner()).await {
// Ok(resp) => match resp {
// BackupCreateResponse::Success => HttpResponse::Ok().finish(),
// },
// Err(e) => {
// error!("While handling /backup/create [POST] request: {e}");
// HttpResponse::InternalServerError().finish()
// }
// }
// }
#[post("/backup/preset")]
async fn backup_preset_post(
data: web::Data<State>,
body: web::Json<BackupPresetPostRequest>,
) -> impl Responder {
match handlers::backup_preset_post(&data.pool, body.into_inner()).await {
Ok(resp) => match resp {
BackupPresetPostResponse::Success => HttpResponse::Ok().finish(),
BackupPresetPostResponse::Conflict => HttpResponse::Conflict().finish(),
},
Err(e) => {
error!("While handling /backup/preset [POST] request: {e}");
HttpResponse::InternalServerError().finish()
}
}
}

37
src/api/data.rs Normal file
View File

@ -0,0 +1,37 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct BackupConfigDocker {}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct BackupConfig {
pub nginx_config: bool,
pub mail_server: bool,
pub docker: Option<BackupConfigDocker>,
}
#[derive(Debug, Deserialize)]
pub enum BackupCreateRequest {
Preset(String),
Config(BackupConfig),
}
#[derive(Debug, Serialize)]
pub enum BackupCreateResponse {
Success,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct BackupPresetPostRequest {
pub id: String,
pub description: Option<String>,
pub config: BackupConfig,
}
#[derive(Debug, Serialize)]
pub enum BackupPresetPostResponse {
Success,
Conflict,
}

31
src/api/handlers.rs Normal file
View File

@ -0,0 +1,31 @@
use crate::api::data::*;
use crate::backend::backup;
use anyhow::Result;
use log::warn;
use sqlx::sqlite::SqlitePool;
// pub async fn backup_create(
// pool: &SqlitePool,
// request: BackupCreateRequest,
// ) -> Result<BackupCreateResponse> {
// Ok(BackupCreateResponse::Success)
// }
pub async fn backup_preset_post(
pool: &SqlitePool,
request: BackupPresetPostRequest,
) -> Result<BackupPresetPostResponse> {
match backup::preset::Preset::load(pool, &request.id).await? {
Some(_) => {
warn!(
"Failed to add Backup Preset: '{}' -> Conflicting name!",
&request.id
);
Ok(BackupPresetPostResponse::Conflict)
}
None => {
backup::preset::Preset::new(pool, request.into()).await?;
Ok(BackupPresetPostResponse::Success)
}
}
}

29
src/api/mod.rs Normal file
View File

@ -0,0 +1,29 @@
pub mod calls;
pub mod data;
mod handlers;
use actix_web::{web, App, HttpServer};
use anyhow::Result;
use sqlx::sqlite::SqlitePool;
pub struct State {
pool: SqlitePool,
token: String,
}
pub async fn start(port: u16, pool: SqlitePool, token: String) -> Result<()> {
let _ = HttpServer::new(move || {
App::new()
// .service(calls::backup_create)
.service(calls::backup_preset_post)
.app_data(web::Data::new(State {
pool: pool.clone(),
token: token.to_owned(),
}))
})
.bind(("0.0.0.0", port))?
.run()
.await;
Ok(())
}

35
src/backend/backup/mod.rs Normal file
View File

@ -0,0 +1,35 @@
pub mod preset;
use crate::api;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct DockerConfig {}
#[derive(Serialize, Deserialize)]
pub struct Config {
nginx_config: bool,
mail_server: bool,
#[serde(skip_serializing_if = "Option::is_none")]
docker: Option<DockerConfig>,
}
impl From<api::data::BackupConfigDocker> for DockerConfig {
fn from(_value: api::data::BackupConfigDocker) -> Self {
Self {}
}
}
impl From<api::data::BackupConfig> for Config {
fn from(value: api::data::BackupConfig) -> Self {
Self {
nginx_config: value.nginx_config,
mail_server: value.mail_server,
docker: match value.docker {
None => None,
Some(cfg) => Some(cfg.into()),
},
}
}
}

View File

@ -0,0 +1,77 @@
use crate::{api, backend::backup};
use anyhow::{Error, Result};
use log::info;
use sqlx::SqlitePool;
struct RawPreset {
id: String,
description: Option<String>,
config: String,
}
pub struct Preset {
id: String,
description: Option<String>,
config: backup::Config,
}
impl From<api::data::BackupPresetPostRequest> for Preset {
fn from(value: api::data::BackupPresetPostRequest) -> Self {
Self {
id: value.id,
description: value.description,
config: value.config.into(),
}
}
}
impl TryFrom<RawPreset> for Preset {
type Error = Error;
fn try_from(value: RawPreset) -> std::result::Result<Self, Self::Error> {
Ok(Self {
id: value.id,
description: value.description,
config: serde_json::from_str(&value.config)?,
})
}
}
impl Preset {
pub async fn new(pool: &SqlitePool, preset: Preset) -> Result<Self> {
let config = serde_json::to_string(&preset.config)?;
sqlx::query!(
r#"INSERT INTO Presets (id, description, config) VALUES ($1, $2, $3);"#,
preset.id,
preset.description,
config
)
.execute(pool)
.await?;
info!(
"Added Backup Preset: '{}' -> {}",
&preset.id,
match &preset.description {
Some(desc) => desc,
None => "<No Description available>",
}
);
Ok(preset)
}
pub async fn load(pool: &SqlitePool, id: &str) -> Result<Option<Self>> {
let query_result =
sqlx::query_as!(RawPreset, r#"SELECT * FROM Presets WHERE id = $1;"#, id)
.fetch_one(pool)
.await;
match query_result {
Ok(raw) => Ok(Some(raw.try_into()?)),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => Err(Error::new(e)),
}
}
}

20
src/backend/mod.rs Normal file
View File

@ -0,0 +1,20 @@
pub mod backup;
use anyhow::Result;
use sqlx::SqlitePool;
pub async fn prepare(pool: &SqlitePool) -> Result<()> {
sqlx::query!(
r#"
CREATE TABLE IF NOT EXISTS Presets (
id VARCHAR(32) NOT NULL,
description TEXT,
config TEXT NOT NULL,
PRIMARY KEY(id)
);
"#
)
.execute(pool)
.await?;
Ok(())
}

30
src/main.rs Normal file
View File

@ -0,0 +1,30 @@
mod api;
mod backend;
use anyhow::Result;
use log::info;
use sqlx::SqlitePool;
#[tokio::main]
async fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
info!(
"Starting Nerdcult Admin Worker v{}",
env!("CARGO_PKG_VERSION")
);
let token = &std::env::var("NC_AW_TOKEN")
.map_err(|_| anyhow::Error::msg("Environment variable NC_AW_TOKEN needs to be set!"))?;
let pool =
SqlitePool::connect(&std::env::var("DATABASE_URL").map_err(|_| {
anyhow::Error::msg("Environment variable DATABASE_URL needs to be set!")
})?)
.await?;
backend::prepare(&pool).await?;
api::start(6969, pool, token.to_string()).await?;
Ok(())
}