init
This commit is contained in:
26
dollhouse/crates/dollhouse-backend/Cargo.toml
Executable file
26
dollhouse/crates/dollhouse-backend/Cargo.toml
Executable file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "dollhouse-backend"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-cors = "0.7.1"
|
||||
dollhouse-db = { path = "../dollhouse-db" }
|
||||
dollhouse-api-types = { path = "../dollhouse-api-types" }
|
||||
actix-session = { version = "0.11.0", features = ["cookie-session"] }
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
actix-web = "4.11.0"
|
||||
argon2 = "0.5.3"
|
||||
env_logger = "0.11.8"
|
||||
log = "0.4.28"
|
||||
serde = "1.0.228"
|
||||
serde_json = "1.0.145"
|
||||
thiserror = "2.0.17"
|
||||
tokio = "1.48.0"
|
||||
mlua = { version = "0.11.4", features = ["lua53", "async", "send"] }
|
||||
rand = "0.9.2"
|
||||
actix-multipart = "0.7.2"
|
||||
uuid = { workspace = true }
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.42"
|
||||
sha2 = "0.10.9"
|
||||
159
dollhouse/crates/dollhouse-backend/src/conversions.rs
Executable file
159
dollhouse/crates/dollhouse-backend/src/conversions.rs
Executable file
@@ -0,0 +1,159 @@
|
||||
use dollhouse_api_types::ReplicantGender as ApiReplicantGender;
|
||||
use dollhouse_api_types::ReplicantStatus as ApiReplicantStatus;
|
||||
use dollhouse_api_types::UserRole as ApiUserRole;
|
||||
use dollhouse_db::ReplicantGender as DbReplicantGender;
|
||||
use dollhouse_db::ReplicantStatus as DbReplicantStatus;
|
||||
use dollhouse_db::UserRole as DbUserRole;
|
||||
|
||||
pub trait UserRoleConvert {
|
||||
fn to_db_role(&self) -> Result<DbUserRole, &'static str>;
|
||||
fn to_api_role(&self) -> Result<ApiUserRole, &'static str>;
|
||||
}
|
||||
|
||||
impl UserRoleConvert for DbUserRole {
|
||||
fn to_db_role(&self) -> Result<DbUserRole, &'static str> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
|
||||
fn to_api_role(&self) -> Result<ApiUserRole, &'static str> {
|
||||
match self {
|
||||
DbUserRole::CorpAdmin => Ok(ApiUserRole::CorpAdmin),
|
||||
DbUserRole::User => Ok(ApiUserRole::User),
|
||||
_ => Err("Unknown user role"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserRoleConvert for ApiUserRole {
|
||||
fn to_db_role(&self) -> Result<DbUserRole, &'static str> {
|
||||
match self {
|
||||
ApiUserRole::CorpAdmin => Ok(DbUserRole::CorpAdmin),
|
||||
ApiUserRole::User => Ok(DbUserRole::User),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_api_role(&self) -> Result<ApiUserRole, &'static str> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ReplicantStatusConvert {
|
||||
fn to_db_status(&self) -> Result<DbReplicantStatus, &'static str>;
|
||||
fn to_api_status(&self) -> Result<ApiReplicantStatus, &'static str>;
|
||||
}
|
||||
|
||||
impl ReplicantStatusConvert for DbReplicantStatus {
|
||||
fn to_db_status(&self) -> Result<DbReplicantStatus, &'static str> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
|
||||
fn to_api_status(&self) -> Result<ApiReplicantStatus, &'static str> {
|
||||
match self {
|
||||
DbReplicantStatus::Active => Ok(ApiReplicantStatus::Active),
|
||||
DbReplicantStatus::Decommissioned => Ok(ApiReplicantStatus::Decommissioned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplicantStatusConvert for ApiReplicantStatus {
|
||||
fn to_db_status(&self) -> Result<DbReplicantStatus, &'static str> {
|
||||
match self {
|
||||
ApiReplicantStatus::Active => Ok(DbReplicantStatus::Active),
|
||||
ApiReplicantStatus::Decommissioned => Ok(DbReplicantStatus::Decommissioned),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_api_status(&self) -> Result<ApiReplicantStatus, &'static str> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ReplicantGenderConvert {
|
||||
fn to_db_gender(&self) -> Result<DbReplicantGender, &'static str>;
|
||||
fn to_api_gender(&self) -> Result<ApiReplicantGender, &'static str>;
|
||||
}
|
||||
|
||||
impl ReplicantGenderConvert for DbReplicantGender {
|
||||
fn to_db_gender(&self) -> Result<DbReplicantGender, &'static str> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
|
||||
fn to_api_gender(&self) -> Result<ApiReplicantGender, &'static str> {
|
||||
match self {
|
||||
DbReplicantGender::Male => Ok(ApiReplicantGender::Male),
|
||||
DbReplicantGender::Female => Ok(ApiReplicantGender::Female),
|
||||
DbReplicantGender::NonBinary => Ok(ApiReplicantGender::NonBinary),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplicantGenderConvert for ApiReplicantGender {
|
||||
fn to_db_gender(&self) -> Result<DbReplicantGender, &'static str> {
|
||||
match self {
|
||||
ApiReplicantGender::Male => Ok(DbReplicantGender::Male),
|
||||
ApiReplicantGender::Female => Ok(DbReplicantGender::Female),
|
||||
ApiReplicantGender::NonBinary => Ok(DbReplicantGender::NonBinary),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_api_gender(&self) -> Result<ApiReplicantGender, &'static str> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ReplicantStatusConvert for Option<T>
|
||||
where
|
||||
T: ReplicantStatusConvert,
|
||||
{
|
||||
fn to_db_status(&self) -> Result<DbReplicantStatus, &'static str> {
|
||||
match self {
|
||||
Some(status) => status.to_db_status(),
|
||||
None => Err("Status is None"),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_api_status(&self) -> Result<ApiReplicantStatus, &'static str> {
|
||||
match self {
|
||||
Some(status) => status.to_api_status(),
|
||||
None => Err("Status is None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ReplicantGenderConvert for Option<T>
|
||||
where
|
||||
T: ReplicantGenderConvert,
|
||||
{
|
||||
fn to_db_gender(&self) -> Result<DbReplicantGender, &'static str> {
|
||||
match self {
|
||||
Some(gender) => gender.to_db_gender(),
|
||||
None => Err("Gender is None"),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_api_gender(&self) -> Result<ApiReplicantGender, &'static str> {
|
||||
match self {
|
||||
Some(gender) => gender.to_api_gender(),
|
||||
None => Err("Gender is None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UserRoleConvert for Option<T>
|
||||
where
|
||||
T: UserRoleConvert,
|
||||
{
|
||||
fn to_db_role(&self) -> Result<DbUserRole, &'static str> {
|
||||
match self {
|
||||
Some(role) => role.to_db_role(),
|
||||
None => Err("Role is None"),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_api_role(&self) -> Result<ApiUserRole, &'static str> {
|
||||
match self {
|
||||
Some(role) => role.to_api_role(),
|
||||
None => Err("Role is None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
290
dollhouse/crates/dollhouse-backend/src/handlers.rs
Executable file
290
dollhouse/crates/dollhouse-backend/src/handlers.rs
Executable file
@@ -0,0 +1,290 @@
|
||||
use crate::services::*;
|
||||
use crate::utils::AppError;
|
||||
use actix_multipart::form::MultipartForm;
|
||||
use actix_multipart::form::tempfile::TempFile;
|
||||
use actix_session::Session;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::web;
|
||||
use dollhouse_api_types::*;
|
||||
use dollhouse_db::Pool;
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PaginationParams {
|
||||
page: Option<usize>,
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, MultipartForm)]
|
||||
pub struct UploadFirmwareForm {
|
||||
#[multipart(limit = "2MB")]
|
||||
file: TempFile,
|
||||
}
|
||||
|
||||
pub async fn create_user(
|
||||
pool: web::Data<Pool>,
|
||||
data: web::Json<CreateUserRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let req = data.into_inner();
|
||||
let pool = pool.into_inner();
|
||||
let pool_ref = pool.as_ref();
|
||||
|
||||
match AuthService::register(pool_ref, req).await {
|
||||
Ok(()) => {
|
||||
log::info!("User created successfully");
|
||||
Ok(HttpResponse::Created().finish())
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Registration error: {:?}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login_user(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
data: web::Json<CreateUserRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let req = data.into_inner();
|
||||
let pool = pool.into_inner();
|
||||
match AuthService::login(&pool, req).await {
|
||||
Ok(user) => {
|
||||
session
|
||||
.insert("user_id", &user.id)
|
||||
.map_err(|_| AppError::InternalServerError)?;
|
||||
session
|
||||
.insert("role", &user.role)
|
||||
.map_err(|_| AppError::InternalServerError)?;
|
||||
session
|
||||
.insert("username", &user.username)
|
||||
.map_err(|_| AppError::InternalServerError)?;
|
||||
session
|
||||
.insert("corp_id", &user.corp_id)
|
||||
.map_err(|_| AppError::InternalServerError)?;
|
||||
Ok(HttpResponse::Ok().json(user))
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Login error: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn logout_user(mut session: Session) -> Result<HttpResponse, AppError> {
|
||||
AuthService::logout(&mut session);
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
pub async fn get_current_user(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = AuthService::check_session(session.clone())?;
|
||||
let user = UserService::get_user(&pool.into_inner(), user_id).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(user))
|
||||
}
|
||||
|
||||
pub async fn create_corp(
|
||||
pool: web::Data<Pool>,
|
||||
data: web::Json<CreateCorpRequest>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let req = data.into_inner();
|
||||
let user_id = AuthService::check_session(session)?;
|
||||
match CorpService::create(&pool.into_inner(), user_id, req).await {
|
||||
Ok(corp) => Ok(HttpResponse::Ok().json(corp)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_user_corp(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let session_user_id = AuthService::check_session(session)?;
|
||||
if session_user_id != path.into_inner() {
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
match CorpService::get_user_corp(&pool.into_inner(), session_user_id).await {
|
||||
Ok(corp) => Ok(HttpResponse::Ok().json(corp)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn join_corp(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
path: web::Path<Uuid>,
|
||||
data: web::Json<JoinCorpRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let session_user_id = AuthService::check_session(session)?;
|
||||
let requested_user_id = path.into_inner();
|
||||
if session_user_id != requested_user_id {
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
|
||||
match CorpService::join(&pool.into_inner(), session_user_id, &data.into_inner()).await {
|
||||
Ok(()) => Ok(HttpResponse::Ok().finish()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_replicant(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
data: web::Json<CreateReplicantRequest>,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = AuthService::check_session(session)?;
|
||||
match ReplicantService::create(
|
||||
&pool.into_inner(),
|
||||
user_id,
|
||||
path.into_inner(),
|
||||
data.into_inner(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(replicant) => Ok(HttpResponse::Ok().json(replicant)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_replicant(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = AuthService::check_session(session)?;
|
||||
match ReplicantService::get_replicant(&pool.into_inner(), user_id, path.into_inner()).await {
|
||||
Ok(replicant) => Ok(HttpResponse::Ok().json(replicant)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_replicants(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
query: web::Query<PaginationParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.limit.unwrap_or(10);
|
||||
let offset = (page - 1) * page_size;
|
||||
let _ = AuthService::check_session(session)?;
|
||||
match ReplicantService::get_replicants(&pool.into_inner(), page_size, offset).await {
|
||||
Ok(replicants) => Ok(HttpResponse::Ok().json(replicants)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upload_replicant_firmware(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
path: web::Path<Uuid>,
|
||||
MultipartForm(form): MultipartForm<UploadFirmwareForm>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = AuthService::check_session(session)?;
|
||||
let pool = pool.into_inner();
|
||||
match ReplicantService::load_firmware(pool.as_ref(), user_id, path.into_inner(), form.file)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(HttpResponse::Ok().finish()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_corp_replicants(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
path: web::Path<Uuid>,
|
||||
query: web::Query<PaginationParams>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.limit.unwrap_or(10);
|
||||
let offset = (page - 1) * page_size;
|
||||
let user_id = AuthService::check_session(session)?;
|
||||
match ReplicantService::get_corp_replicants(
|
||||
&pool.into_inner(),
|
||||
user_id,
|
||||
path.into_inner(),
|
||||
page_size,
|
||||
offset,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(replicants) => Ok(HttpResponse::Ok().json(replicants)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn change_replicant_privacy(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
path: web::Path<Uuid>,
|
||||
data: web::Json<ChangePrivacyRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = AuthService::check_session(session)?;
|
||||
match ReplicantService::change_privacy(
|
||||
&pool.into_inner(),
|
||||
user_id,
|
||||
path.into_inner(),
|
||||
data.into_inner().is_private,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(HttpResponse::Ok().finish()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn change_replicant_owner(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
path: web::Path<Uuid>,
|
||||
data: web::Json<ChangeReplicantOwnerRequest>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = AuthService::check_session(session)?;
|
||||
let replicant_id = path.into_inner();
|
||||
match ReplicantService::change_owner(
|
||||
&pool.into_inner(),
|
||||
user_id,
|
||||
replicant_id,
|
||||
data.into_inner().new_corp,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(HttpResponse::Ok().finish()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_firmware(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = AuthService::check_session(session)?;
|
||||
let pool = pool.into_inner();
|
||||
|
||||
match LuaService::run(&pool, user_id, path.into_inner()).await {
|
||||
Ok(output) => Ok(HttpResponse::Ok().json(output)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_firmware(
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
path: web::Path<Uuid>,
|
||||
) -> Result<HttpResponse, AppError> {
|
||||
let user_id = AuthService::check_session(session)?;
|
||||
let pool = pool.into_inner();
|
||||
|
||||
match ReplicantService::download_firmware(&pool, user_id, path.into_inner()).await {
|
||||
Ok(firmware) => Ok(HttpResponse::Ok().json(firmware)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
99
dollhouse/crates/dollhouse-backend/src/main.rs
Executable file
99
dollhouse/crates/dollhouse-backend/src/main.rs
Executable file
@@ -0,0 +1,99 @@
|
||||
use crate::utils::AppError;
|
||||
use actix_cors::Cors;
|
||||
use actix_multipart::form::MultipartFormConfig;
|
||||
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
|
||||
use actix_web::cookie::Key;
|
||||
use actix_web::middleware::Logger;
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use dollhouse_db::create_db_pool;
|
||||
use env_logger::Env;
|
||||
use log;
|
||||
|
||||
mod conversions;
|
||||
mod handlers;
|
||||
mod services;
|
||||
mod utils;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("debug"))
|
||||
.format_timestamp_millis()
|
||||
.format_module_path(false)
|
||||
.format_target(false)
|
||||
.init();
|
||||
|
||||
log::info!("Starting server");
|
||||
|
||||
let db_pool = create_db_pool().await;
|
||||
|
||||
let key = Key::generate();
|
||||
|
||||
HttpServer::new(move || {
|
||||
let key = key.clone();
|
||||
let cors = Cors::permissive();
|
||||
App::new()
|
||||
.wrap(
|
||||
SessionMiddleware::builder(CookieSessionStore::default(), key)
|
||||
.cookie_secure(false)
|
||||
.cookie_http_only(false)
|
||||
.cookie_same_site(actix_web::cookie::SameSite::Lax)
|
||||
.cookie_name("session".to_string())
|
||||
.cookie_path("/".to_string())
|
||||
.build(),
|
||||
)
|
||||
.wrap(Logger::new("%a %t \"%r\" %s"))
|
||||
.app_data(web::Data::new(db_pool.clone()))
|
||||
.app_data(
|
||||
web::PathConfig::default().error_handler(|_, _| AppError::InvalidUuidFormat.into()),
|
||||
)
|
||||
.app_data(
|
||||
MultipartFormConfig::default()
|
||||
.total_limit(10 * 1024 * 1024)
|
||||
.error_handler(|err, _| {
|
||||
log::error!("Multipart error: {:?}", err);
|
||||
AppError::MultipartError(err.to_string()).into()
|
||||
}),
|
||||
)
|
||||
.service(
|
||||
web::scope("/api")
|
||||
.route("/auth/login", web::post().to(handlers::login_user))
|
||||
.route("/auth/logout", web::post().to(handlers::logout_user))
|
||||
.route("/auth/register", web::post().to(handlers::create_user))
|
||||
.route("/auth/me", web::get().to(handlers::get_current_user))
|
||||
.route("/user/{id}/corp", web::get().to(handlers::get_user_corp))
|
||||
.route("/user/{id}/corp", web::post().to(handlers::create_corp))
|
||||
.route("/user/{id}/join-corp", web::post().to(handlers::join_corp))
|
||||
.route(
|
||||
"/corp/{id}/replicants",
|
||||
web::get().to(handlers::get_corp_replicants),
|
||||
)
|
||||
.route(
|
||||
"/corp/{id}/replicant",
|
||||
web::post().to(handlers::create_replicant),
|
||||
)
|
||||
.route("/replicants", web::get().to(handlers::get_replicants))
|
||||
.route(
|
||||
"/replicant/{id}/firmware",
|
||||
web::post().to(handlers::upload_replicant_firmware),
|
||||
)
|
||||
.route("/replicant/{id}", web::get().to(handlers::get_replicant))
|
||||
.route(
|
||||
"/replicant/{id}/change-privacy",
|
||||
web::post().to(handlers::change_replicant_privacy),
|
||||
)
|
||||
.route(
|
||||
"/replicant/{id}/change-owner",
|
||||
web::post().to(handlers::change_replicant_owner),
|
||||
)
|
||||
.route("/replicant/{id}/run", web::get().to(handlers::run_firmware))
|
||||
.route(
|
||||
"/replicant/{id}/firmware",
|
||||
web::get().to(handlers::download_firmware),
|
||||
),
|
||||
)
|
||||
.wrap(cors)
|
||||
})
|
||||
.bind("0.0.0.0:5555")?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
124
dollhouse/crates/dollhouse-backend/src/services/auth.rs
Executable file
124
dollhouse/crates/dollhouse-backend/src/services/auth.rs
Executable file
@@ -0,0 +1,124 @@
|
||||
use crate::conversions::UserRoleConvert;
|
||||
use crate::utils::*;
|
||||
use actix_session::Session;
|
||||
use argon2::Argon2;
|
||||
use argon2::password_hash::rand_core::OsRng;
|
||||
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
|
||||
use dollhouse_api_types::{CreateUserRequest, UserResponse, UserRole};
|
||||
use dollhouse_db::{AsyncPgConnection, NewUser, Pool, repositories::UserRepository};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct AuthService;
|
||||
|
||||
impl AuthService {
|
||||
pub async fn login(pool: &Pool, req: CreateUserRequest) -> Result<UserResponse, AppError> {
|
||||
let mut conn = pool.get().await.map_err(|_| AppError::RepositoryError)?;
|
||||
let user = UserRepository::find_by_username(&mut conn, &req.username)
|
||||
.await
|
||||
.map_err(|_| AppError::RepositoryError)?;
|
||||
|
||||
match user {
|
||||
Some(user) => {
|
||||
if Self::verify_password(&user.password, &req.password)? {
|
||||
Ok(UserResponse {
|
||||
id: user.id,
|
||||
role: user.role.to_api_role().unwrap_or(UserRole::User),
|
||||
username: user.username,
|
||||
corp_id: user.corp_id,
|
||||
})
|
||||
} else {
|
||||
Err(AppError::Unauthorized)
|
||||
}
|
||||
}
|
||||
None => Err(AppError::Unauthorized),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logout(session: &mut Session) {
|
||||
session.remove("user_id");
|
||||
session.remove("username");
|
||||
session.remove("role");
|
||||
}
|
||||
|
||||
pub async fn register(pool: &Pool, req: CreateUserRequest) -> Result<(), AppError> {
|
||||
let hashed_password = Self::hash_password(&req.password)?;
|
||||
let mut conn = pool.get().await.map_err(|_| AppError::RepositoryError)?;
|
||||
|
||||
if Self::is_username_taken(&mut conn, &req.username).await? {
|
||||
return Err(AppError::BadRequest("Username already exists".to_string()));
|
||||
}
|
||||
|
||||
let new_user = NewUser {
|
||||
username: req.username,
|
||||
password: hashed_password,
|
||||
};
|
||||
|
||||
UserRepository::create_user(&mut conn, new_user)
|
||||
.await
|
||||
.map_err(|_| AppError::RepositoryError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn is_username_taken(
|
||||
conn: &mut AsyncPgConnection,
|
||||
username: &str,
|
||||
) -> Result<bool, AppError> {
|
||||
let user = UserRepository::find_by_username(conn, username)
|
||||
.await
|
||||
.map_err(|_| AppError::RepositoryError)?;
|
||||
Ok(user.is_some())
|
||||
}
|
||||
|
||||
fn hash_password(password: &str) -> Result<String, PasswordError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
match argon2.hash_password(password.as_bytes(), &salt) {
|
||||
Ok(hash) => {
|
||||
log::debug!("Password hashed successfully");
|
||||
Ok(hash.to_string())
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
log::error!("Password hashing failed: {}", error_msg);
|
||||
Err(PasswordError::HashError(error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_session(session: Session) -> Result<Uuid, AppError> {
|
||||
match session.get::<Uuid>("user_id") {
|
||||
Ok(Some(id)) => Ok(id),
|
||||
Ok(None) => Err(AppError::Unauthorized),
|
||||
Err(_) => Err(AppError::InternalServerError),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_password(hash: &str, password: &str) -> Result<bool, PasswordError> {
|
||||
match PasswordHash::new(hash) {
|
||||
Ok(parsed_hash) => {
|
||||
match Argon2::default().verify_password(password.as_bytes(), &parsed_hash) {
|
||||
Ok(_) => {
|
||||
log::debug!("Password verification successful");
|
||||
Ok(true)
|
||||
}
|
||||
Err(argon2::password_hash::Error::Password) => {
|
||||
log::debug!("Password verification failed - incorrect password");
|
||||
Ok(false)
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
log::error!("Password verification error: {}", error_msg);
|
||||
Err(PasswordError::VerificationFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = e.to_string();
|
||||
log::error!("Failed to parse password hash: {}", error_msg);
|
||||
Err(PasswordError::HashError(error_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
114
dollhouse/crates/dollhouse-backend/src/services/corp.rs
Executable file
114
dollhouse/crates/dollhouse-backend/src/services/corp.rs
Executable file
@@ -0,0 +1,114 @@
|
||||
use crate::{conversions::UserRoleConvert, utils::AppError};
|
||||
use base64::prelude::*;
|
||||
use dollhouse_api_types::{CorpResponse, CreateCorpRequest, JoinCorpRequest, StaffResponse};
|
||||
use dollhouse_db::{
|
||||
NewCorp, Pool,
|
||||
errors::DbError,
|
||||
repositories::{UserRepository, corp::CorpRepository},
|
||||
};
|
||||
use rand::Rng;
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::{Context, Timestamp, Uuid};
|
||||
|
||||
pub struct CorpService;
|
||||
|
||||
impl CorpService {
|
||||
pub async fn create(
|
||||
pool: &Pool,
|
||||
user_id: Uuid,
|
||||
req: CreateCorpRequest,
|
||||
) -> Result<CorpResponse, AppError> {
|
||||
let mut rng = rand::rng();
|
||||
let count = rng.random::<u16>();
|
||||
|
||||
let corp_id = Self::gen_uuid(count);
|
||||
let invite_code = Self::gen_uuid(count + 1);
|
||||
|
||||
let new_corp = NewCorp {
|
||||
id: corp_id,
|
||||
invite_code: BASE64_STANDARD.encode(&invite_code.to_string()),
|
||||
description: req.description.clone(),
|
||||
name: req.name.clone(),
|
||||
};
|
||||
|
||||
let mut conn = pool.get().await.map_err(|_| AppError::RepositoryError)?;
|
||||
match CorpRepository::create(&mut conn, new_corp).await {
|
||||
Err(e) => {
|
||||
log::error!("Some error during creating corp: {}", e);
|
||||
Err(AppError::InternalServerError)
|
||||
}
|
||||
Ok(new_corp) => {
|
||||
UserRepository::update_role(&mut conn, user_id, dollhouse_db::UserRole::CorpAdmin)
|
||||
.await?;
|
||||
UserRepository::update_corp_id(&mut conn, user_id, Some(new_corp.id)).await?;
|
||||
let staff = CorpRepository::get_corp_user_ids_with_names(&mut conn, new_corp.id)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|(id, name, role)| StaffResponse {
|
||||
id: *id,
|
||||
username: name.clone(),
|
||||
role: role.to_api_role().unwrap(),
|
||||
})
|
||||
.collect();
|
||||
Ok(CorpResponse {
|
||||
id: new_corp.id,
|
||||
invite_code: new_corp.invite_code,
|
||||
staff,
|
||||
description: req.description,
|
||||
name: req.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_uuid(rng: u16) -> Uuid {
|
||||
let context = Context::new(rng);
|
||||
let ts = Timestamp::now(&context);
|
||||
let (ticks, counter) = ts.to_gregorian();
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
|
||||
hasher.update(ticks.to_be_bytes());
|
||||
hasher.update(counter.to_be_bytes());
|
||||
|
||||
let hash = hasher.finalize();
|
||||
let node_id = hash[hash.len() - 6..].try_into().unwrap();
|
||||
|
||||
Uuid::new_v1(ts, &node_id)
|
||||
}
|
||||
|
||||
pub async fn get_user_corp(pool: &Pool, user_id: Uuid) -> Result<CorpResponse, AppError> {
|
||||
let mut conn = pool.get().await.map_err(|_| AppError::RepositoryError)?;
|
||||
match UserRepository::get_user_corp(&mut conn, user_id).await {
|
||||
Ok(Some(corp)) => {
|
||||
let staff = CorpRepository::get_corp_user_ids_with_names(&mut conn, corp.id)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|(id, name, role)| StaffResponse {
|
||||
id: *id,
|
||||
username: name.clone(),
|
||||
role: role.to_api_role().unwrap(),
|
||||
})
|
||||
.collect();
|
||||
Ok(CorpResponse {
|
||||
id: corp.id,
|
||||
name: corp.name,
|
||||
description: corp.description,
|
||||
staff,
|
||||
invite_code: corp.invite_code,
|
||||
})
|
||||
}
|
||||
Ok(None) => Err(AppError::NotFound),
|
||||
Err(_) => Err(AppError::RepositoryError),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn join(pool: &Pool, user_id: Uuid, req: &JoinCorpRequest) -> Result<(), AppError> {
|
||||
let mut conn = pool.get().await.map_err(|_| AppError::RepositoryError)?;
|
||||
match CorpRepository::join_by_invite(&mut conn, user_id, req.invite_code.as_str()).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(DbError::NotFound) => Err(AppError::NotFound),
|
||||
Err(_) => Err(AppError::RepositoryError),
|
||||
}
|
||||
}
|
||||
}
|
||||
226
dollhouse/crates/dollhouse-backend/src/services/lua.rs
Executable file
226
dollhouse/crates/dollhouse-backend/src/services/lua.rs
Executable file
@@ -0,0 +1,226 @@
|
||||
use crate::{services::replicant::ReplicantService, utils::AppError};
|
||||
use base64::prelude::*;
|
||||
use dollhouse_api_types::FirmwareOutputResponse;
|
||||
use dollhouse_db::{Pool, repositories::ReplicantRepository};
|
||||
use mlua::{Lua, Table, Value};
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{Duration, timeout};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct LuaService;
|
||||
|
||||
const MEMORY_LIMIT: usize = 10 * 1024 * 1024;
|
||||
const TIME_LIMIT_MS: u64 = 1000;
|
||||
|
||||
impl LuaService {
|
||||
fn create_lua_instance() -> Result<Lua, AppError> {
|
||||
let lua = Lua::new();
|
||||
lua.set_memory_limit(MEMORY_LIMIT)?;
|
||||
Ok(lua)
|
||||
}
|
||||
|
||||
fn setup_sandbox(lua: &Lua) -> Result<(), AppError> {
|
||||
let globals = lua.globals();
|
||||
|
||||
let dangerous_libs = [
|
||||
"os",
|
||||
"io",
|
||||
"debug",
|
||||
"load",
|
||||
"loadstring",
|
||||
"dofile",
|
||||
"loadfile",
|
||||
];
|
||||
for lib in &dangerous_libs {
|
||||
globals.set(*lib, Value::Nil)?;
|
||||
}
|
||||
|
||||
let g_mt = lua.create_table()?;
|
||||
|
||||
let allowed_globals = vec![
|
||||
"_VERSION",
|
||||
"print",
|
||||
"type",
|
||||
"assert",
|
||||
"error",
|
||||
"pairs",
|
||||
"ipairs",
|
||||
"next",
|
||||
"select",
|
||||
"pcall",
|
||||
"xpcall",
|
||||
"table",
|
||||
"string",
|
||||
"math",
|
||||
"tonumber",
|
||||
"tostring",
|
||||
"setmetatable",
|
||||
"getmetatable",
|
||||
"rawset",
|
||||
"rawget",
|
||||
"rawequal",
|
||||
];
|
||||
|
||||
let allowed_globals_clone1 = allowed_globals.clone();
|
||||
|
||||
g_mt.set(
|
||||
"__newindex",
|
||||
lua.create_function(move |_, (t, name, value): (Table, String, Value)| {
|
||||
if !allowed_globals_clone1.contains(&name.as_str()) {
|
||||
return Err(mlua::Error::RuntimeError(format!(
|
||||
"Security: creating global '{}' is not allowed",
|
||||
name
|
||||
)));
|
||||
}
|
||||
|
||||
t.raw_set(name, value)?;
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
let allowed_globals_clone2 = allowed_globals.clone();
|
||||
let dangerous = vec!["io", "os", "debug", "package"];
|
||||
|
||||
g_mt.set(
|
||||
"__index",
|
||||
lua.create_function(move |lua, (t, name): (Table, String)| {
|
||||
if dangerous.contains(&name.as_str()) {
|
||||
return Err(mlua::Error::RuntimeError(format!(
|
||||
"Security: access to '{}' is prohibited",
|
||||
name
|
||||
)));
|
||||
}
|
||||
|
||||
if allowed_globals_clone2.contains(&name.as_str()) {
|
||||
let globals = lua.globals();
|
||||
return Ok(globals.raw_get::<Value>(name)?);
|
||||
}
|
||||
|
||||
Ok(Value::Nil)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
globals.set_metatable(Some(g_mt));
|
||||
|
||||
Self::setup_safe_print(lua)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_safe_print(lua: &Lua) -> Result<(), AppError> {
|
||||
let safe_print = lua.create_function(|_, args: mlua::MultiValue| {
|
||||
let output: String = args
|
||||
.into_iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.join("\t");
|
||||
|
||||
println!("{}", output);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
lua.globals().set("print", safe_print)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_with_timeout(
|
||||
lua: Arc<Lua>,
|
||||
bytecode: Vec<u8>,
|
||||
) -> Result<mlua::Value, AppError> {
|
||||
let task = tokio::task::spawn_blocking(move || {
|
||||
let lua_clone = Arc::clone(&lua);
|
||||
lua_clone
|
||||
.load(&bytecode)
|
||||
.set_name("[[user_firmware]]")
|
||||
.eval()
|
||||
});
|
||||
|
||||
match timeout(Duration::from_millis(TIME_LIMIT_MS), task).await {
|
||||
Ok(Ok(result)) => result.map_err(|e| {
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("not enough memory") {
|
||||
log::error!("Memory limit exceeded");
|
||||
AppError::InternalServerError
|
||||
} else {
|
||||
AppError::LuaExecutionError(e)
|
||||
}
|
||||
}),
|
||||
Ok(Err(join_err)) => {
|
||||
log::error!("Join error: {}", join_err);
|
||||
Err(AppError::InternalServerError)
|
||||
}
|
||||
Err(_) => {
|
||||
tokio::task::yield_now().await;
|
||||
Err(AppError::InternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn value_to_string(value: mlua::Value) -> Result<String, AppError> {
|
||||
match value {
|
||||
mlua::Value::String(s) => Ok(s.to_str()?.to_string()),
|
||||
mlua::Value::Nil => Ok("nil".to_string()),
|
||||
mlua::Value::Boolean(b) => Ok(b.to_string()),
|
||||
mlua::Value::Number(n) => Ok(n.to_string()),
|
||||
mlua::Value::Integer(i) => Ok(i.to_string()),
|
||||
mlua::Value::Table(t) => {
|
||||
let mut parts = Vec::new();
|
||||
for pair in t.pairs::<mlua::Value, mlua::Value>() {
|
||||
let (key, value) = pair?;
|
||||
parts.push(format!("{}: {}", key.to_string()?, value.to_string()?));
|
||||
}
|
||||
Ok(format!("{{{}}}", parts.join(", ")))
|
||||
}
|
||||
_ => Ok(format!("{:?}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
pool: &Pool,
|
||||
user_id: Uuid,
|
||||
replicant_id: Uuid,
|
||||
) -> Result<FirmwareOutputResponse, AppError> {
|
||||
let lua = Arc::new(Self::create_lua_instance()?);
|
||||
|
||||
Self::setup_sandbox(&lua)?;
|
||||
|
||||
let mut conn = pool.get().await.map_err(|_| AppError::RepositoryError)?;
|
||||
let replicant = ReplicantRepository::get(&mut conn, replicant_id)
|
||||
.await
|
||||
.map_err(|_| AppError::RepositoryError)?;
|
||||
|
||||
ReplicantService::check_replicant_access(
|
||||
&mut conn,
|
||||
user_id,
|
||||
replicant.is_private,
|
||||
replicant.corp_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match replicant.firmware_file {
|
||||
Some(filename) => {
|
||||
let firmware_path = std::path::Path::new("firmware").join(filename);
|
||||
let firmware_data = tokio::fs::read(&firmware_path).await.map_err(|e| {
|
||||
log::error!(
|
||||
"Failed to read firmware file from {:?}: {}",
|
||||
firmware_path,
|
||||
e
|
||||
);
|
||||
AppError::InternalServerError
|
||||
})?;
|
||||
|
||||
if firmware_data.is_empty() {
|
||||
return Err(AppError::InternalServerError);
|
||||
}
|
||||
|
||||
let result = Self::execute_with_timeout(Arc::clone(&lua), firmware_data).await?;
|
||||
let mut output = Self::value_to_string(result)?;
|
||||
|
||||
output = BASE64_STANDARD.encode(output);
|
||||
|
||||
Ok(FirmwareOutputResponse { output })
|
||||
}
|
||||
None => Err(AppError::NotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
11
dollhouse/crates/dollhouse-backend/src/services/mod.rs
Executable file
11
dollhouse/crates/dollhouse-backend/src/services/mod.rs
Executable file
@@ -0,0 +1,11 @@
|
||||
pub mod auth;
|
||||
pub mod corp;
|
||||
pub mod lua;
|
||||
pub mod replicant;
|
||||
pub mod user;
|
||||
|
||||
pub use self::auth::*;
|
||||
pub use self::corp::*;
|
||||
pub use self::lua::*;
|
||||
pub use self::replicant::*;
|
||||
pub use self::user::*;
|
||||
466
dollhouse/crates/dollhouse-backend/src/services/replicant.rs
Executable file
466
dollhouse/crates/dollhouse-backend/src/services/replicant.rs
Executable file
@@ -0,0 +1,466 @@
|
||||
use crate::{
|
||||
conversions::{ReplicantGenderConvert, ReplicantStatusConvert},
|
||||
utils::AppError,
|
||||
};
|
||||
use actix_multipart::form::tempfile::TempFile;
|
||||
use base64::prelude::*;
|
||||
use dollhouse_api_types::{CreateReplicantRequest, FirmwareOutputResponse, ReplicantFullResponse};
|
||||
use dollhouse_db::{
|
||||
AsyncPgConnection, NewReplicant, Pool, ReplicantStatus, UserRole,
|
||||
repositories::{CorpRepository, ReplicantRepository, UserRepository},
|
||||
};
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct ReplicantService;
|
||||
|
||||
impl ReplicantService {
|
||||
pub async fn create(
|
||||
pool: &Pool,
|
||||
user_id: Uuid,
|
||||
corp_id: Uuid,
|
||||
data: CreateReplicantRequest,
|
||||
) -> Result<ReplicantFullResponse, AppError> {
|
||||
let mut conn = pool.get().await.map_err(|_| AppError::RepositoryError)?;
|
||||
|
||||
Self::check_user_corp(&mut conn, user_id, corp_id).await?;
|
||||
if !Self::is_admin(&mut conn, user_id).await? {
|
||||
log::warn!(
|
||||
"User {} attempted to create replicant in corp {} without admin rights",
|
||||
user_id,
|
||||
corp_id
|
||||
);
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
|
||||
let new_replicant = NewReplicant {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
status: ReplicantStatus::Active,
|
||||
gender: data.gender.to_db_gender().unwrap(),
|
||||
corp_id,
|
||||
};
|
||||
|
||||
match ReplicantRepository::create(&mut conn, new_replicant).await {
|
||||
Ok(replicant) => Ok(ReplicantFullResponse {
|
||||
id: replicant.id,
|
||||
name: replicant.name,
|
||||
description: replicant.description,
|
||||
health: replicant.health,
|
||||
strength: replicant.strength,
|
||||
intelligence: replicant.intelligence,
|
||||
gender: replicant.gender.to_api_gender().unwrap(),
|
||||
status: replicant.status.to_api_status().unwrap(),
|
||||
is_private: replicant.is_private,
|
||||
firmware_file: replicant.firmware_file,
|
||||
corp_id: replicant.corp_id,
|
||||
}),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_replicant(
|
||||
pool: &Pool,
|
||||
user_id: Uuid,
|
||||
replicant_id: Uuid,
|
||||
) -> Result<ReplicantFullResponse, AppError> {
|
||||
let mut conn = pool.get().await.map_err(|e| {
|
||||
log::error!("Failed to get connection from pool: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
let replicant = ReplicantRepository::get_replicant_full(&mut conn, replicant_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Some error: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
Self::check_replicant_access(&mut conn, user_id, replicant.is_private, replicant.corp_id)
|
||||
.await?;
|
||||
|
||||
Ok(ReplicantFullResponse {
|
||||
id: replicant.id,
|
||||
name: replicant.name,
|
||||
description: replicant.description,
|
||||
health: replicant.health,
|
||||
strength: replicant.strength,
|
||||
intelligence: replicant.intelligence,
|
||||
gender: replicant.gender.to_api_gender().unwrap(),
|
||||
status: replicant.status.to_api_status().unwrap(),
|
||||
is_private: replicant.is_private,
|
||||
firmware_file: replicant.firmware_file,
|
||||
corp_id: replicant.corp_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_replicants(
|
||||
pool: &Pool,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<Vec<ReplicantFullResponse>, AppError> {
|
||||
let mut conn = pool.get().await.map_err(|e| {
|
||||
log::error!("Failed to get connection from pool: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
let replicants = ReplicantRepository::get_much(&mut conn, limit, offset)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to get replicants: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
Ok(replicants
|
||||
.into_iter()
|
||||
.map(|replicant| ReplicantFullResponse {
|
||||
id: replicant.id,
|
||||
name: replicant.name,
|
||||
description: replicant.description,
|
||||
gender: replicant.gender.to_api_gender().unwrap(),
|
||||
status: replicant.status.to_api_status().unwrap(),
|
||||
is_private: replicant.is_private,
|
||||
firmware_file: replicant.firmware_file,
|
||||
health: replicant.health,
|
||||
strength: replicant.strength,
|
||||
intelligence: replicant.intelligence,
|
||||
corp_id: replicant.corp_id,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_corp_replicants(
|
||||
pool: &Pool,
|
||||
user_id: Uuid,
|
||||
corp_id: Uuid,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<Vec<ReplicantFullResponse>, AppError> {
|
||||
let mut conn = pool.get().await.map_err(|e| {
|
||||
log::error!("Failed to get connection from pool: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
Self::check_user_corp(&mut conn, user_id, corp_id).await?;
|
||||
|
||||
let replicants =
|
||||
ReplicantRepository::get_corp_replicants_full(&mut conn, corp_id, limit, offset)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to get corp replicants: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
log::debug!("Number of replicants: {}", replicants.len());
|
||||
|
||||
Ok(replicants
|
||||
.into_iter()
|
||||
.map(|replicant| ReplicantFullResponse {
|
||||
id: replicant.id,
|
||||
name: replicant.name,
|
||||
description: replicant.description,
|
||||
gender: replicant.gender.to_api_gender().unwrap(),
|
||||
status: replicant.status.to_api_status().unwrap(),
|
||||
is_private: replicant.is_private,
|
||||
firmware_file: replicant.firmware_file,
|
||||
corp_id: replicant.corp_id,
|
||||
health: replicant.health,
|
||||
strength: replicant.strength,
|
||||
intelligence: replicant.intelligence,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn change_privacy(
|
||||
pool: &Pool,
|
||||
user_id: Uuid,
|
||||
replicant_id: Uuid,
|
||||
privacy: bool,
|
||||
) -> Result<(), AppError> {
|
||||
let mut conn = pool.get().await.map_err(|e| {
|
||||
log::error!("Failed to get connection from pool: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
let replicant = ReplicantRepository::get(&mut conn, replicant_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to change privacy: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
Self::check_user_corp(&mut conn, user_id, replicant.corp_id).await?;
|
||||
|
||||
let is_admin = Self::is_admin(&mut conn, user_id).await?;
|
||||
if !is_admin {
|
||||
log::warn!(
|
||||
"User {} attempted to change privacy without admin rights",
|
||||
user_id
|
||||
);
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
|
||||
ReplicantRepository::change_privacy(&mut conn, replicant_id, privacy).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn change_owner(
|
||||
pool: &Pool,
|
||||
user_id: Uuid,
|
||||
replicant_id: Uuid,
|
||||
new_owner_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
let mut conn = pool.get().await.map_err(|e| {
|
||||
log::error!("Failed to get connection from pool: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
let replicant = ReplicantRepository::get(&mut conn, replicant_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to change owner: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
Self::check_user_corp(&mut conn, user_id, replicant.corp_id).await?;
|
||||
if !Self::is_admin(&mut conn, user_id).await? {
|
||||
log::warn!(
|
||||
"User {} attempted to change owner of replicant {} without admin rights",
|
||||
user_id,
|
||||
replicant_id
|
||||
);
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
|
||||
let _ = CorpRepository::get_corp(&mut conn, new_owner_id).await.map_err(|e| {
|
||||
log::error!("New corp {} not found: {}", new_owner_id, e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
ReplicantRepository::change_owner(&mut conn, replicant_id, new_owner_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to change owner: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn check_replicant_access(
|
||||
conn: &mut AsyncPgConnection,
|
||||
user_id: Uuid,
|
||||
is_private: bool,
|
||||
corp_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
if !is_private {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let user_corp = UserRepository::get_user_corp(conn, user_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Database error while checking user corp: {}", e);
|
||||
AppError::InternalServerError
|
||||
})?;
|
||||
|
||||
log::debug!("User {} corporation: {:?}", user_id, user_corp);
|
||||
|
||||
match user_corp {
|
||||
Some(corp) if corp.id == corp_id => Ok(()),
|
||||
Some(_) => {
|
||||
log::warn!(
|
||||
"User {} attempted to access unauthorized replicant",
|
||||
user_id
|
||||
);
|
||||
Err(AppError::Unauthorized)
|
||||
}
|
||||
None => {
|
||||
log::warn!("User {} has no corporation", user_id);
|
||||
Err(AppError::Unauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_user_corp(
|
||||
conn: &mut AsyncPgConnection,
|
||||
user_id: Uuid,
|
||||
corp_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
let user_corp = CorpRepository::get_corp_by_user(conn, user_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Database error while checking user corp: {}", e);
|
||||
AppError::InternalServerError
|
||||
})?;
|
||||
|
||||
match user_corp {
|
||||
Some(corp) if corp.id == corp_id => Ok(()),
|
||||
Some(_) => {
|
||||
log::warn!(
|
||||
"User {} attempted to access unauthorized corp {}",
|
||||
user_id,
|
||||
corp_id
|
||||
);
|
||||
Err(AppError::Unauthorized)
|
||||
}
|
||||
None => {
|
||||
log::warn!("User {} has no corporation", user_id);
|
||||
Err(AppError::Unauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_admin(conn: &mut AsyncPgConnection, user_id: Uuid) -> Result<bool, AppError> {
|
||||
let user = UserRepository::get_user(conn, user_id).await?;
|
||||
|
||||
match user {
|
||||
Some(user) => Ok(user.role == UserRole::CorpAdmin),
|
||||
None => {
|
||||
log::warn!("User {} not found while checking admin status", user_id);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_firmware(
|
||||
pool: &Pool,
|
||||
user_id: Uuid,
|
||||
replicant_id: Uuid,
|
||||
firmware: TempFile,
|
||||
) -> Result<(), AppError> {
|
||||
let mut conn = pool.get().await.map_err(|e| {
|
||||
log::error!("Failed to get connection from pool: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
let replicant = ReplicantRepository::get(&mut conn, replicant_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to load firmware: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
let corp_id = replicant.corp_id;
|
||||
|
||||
Self::check_user_corp(&mut conn, user_id, corp_id).await?;
|
||||
if !Self::is_admin(&mut conn, user_id).await? {
|
||||
log::warn!(
|
||||
"User {} attempted to upload firmware for replicant {} without admin rights",
|
||||
user_id,
|
||||
replicant_id
|
||||
);
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
|
||||
let file_content = Self::validate_firmware_file(&firmware)?;
|
||||
|
||||
let filename = format!("firmware_{}", replicant.id);
|
||||
let firmware_dir = Path::new("firmware");
|
||||
|
||||
tokio::fs::create_dir_all(firmware_dir).await.map_err(|e| {
|
||||
log::error!("Failed to create firmware directory: {}", e);
|
||||
AppError::InternalServerError
|
||||
})?;
|
||||
|
||||
let file_path = firmware_dir.join(&filename);
|
||||
tokio::fs::write(&file_path, file_content)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!(
|
||||
"Failed to write firmware file {}: {}",
|
||||
file_path.display(),
|
||||
e
|
||||
);
|
||||
AppError::InternalServerError
|
||||
})?;
|
||||
|
||||
ReplicantRepository::update_firmware(&mut conn, replicant_id, filename)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to update firmware in database: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
log::info!(
|
||||
"Firmware loaded successfully for replicant {} by user {}, file: {}",
|
||||
replicant_id,
|
||||
user_id,
|
||||
file_path.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_firmware_file(firmware_file: &TempFile) -> Result<Vec<u8>, AppError> {
|
||||
let file = firmware_file.file.as_ref();
|
||||
|
||||
let metadata = file.metadata().map_err(|e| {
|
||||
log::error!("Failed to get file metadata: {}", e);
|
||||
AppError::InternalServerError
|
||||
})?;
|
||||
|
||||
if metadata.len() > 10 * 1024 * 1024 {
|
||||
log::warn!("Firmware file too large: {} bytes", metadata.len());
|
||||
return Err(AppError::BadRequest(
|
||||
"Firmware file too large (max 10MB)".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut file_content = Vec::new();
|
||||
let mut file_handle = std::fs::File::open(&file).map_err(|e| {
|
||||
log::error!("Failed to open firmware file: {}", e);
|
||||
AppError::InternalServerError
|
||||
})?;
|
||||
|
||||
file_handle.read_to_end(&mut file_content).map_err(|e| {
|
||||
log::error!("Failed to read firmware file: {}", e);
|
||||
AppError::InternalServerError
|
||||
})?;
|
||||
|
||||
if file_content.is_empty() {
|
||||
log::warn!("Empty firmware file provided");
|
||||
return Err(AppError::BadRequest("Firmware file is empty".to_string()));
|
||||
}
|
||||
|
||||
Ok(file_content)
|
||||
}
|
||||
|
||||
pub async fn download_firmware(
|
||||
pool: &Pool,
|
||||
user_id: Uuid,
|
||||
replicant_id: Uuid,
|
||||
) -> Result<FirmwareOutputResponse, AppError> {
|
||||
let mut conn = pool.get().await.map_err(|e| {
|
||||
log::error!("Failed to get connection from pool: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
|
||||
let replicant = ReplicantRepository::get(&mut conn, replicant_id)
|
||||
.await
|
||||
.map_err(|_| AppError::RepositoryError)?;
|
||||
Self::check_replicant_access(&mut conn, user_id, replicant.is_private, replicant.corp_id)
|
||||
.await?;
|
||||
|
||||
match replicant.firmware_file {
|
||||
Some(filename) => {
|
||||
let firmware_path = std::path::Path::new("firmware").join(filename);
|
||||
let firmware_data = tokio::fs::read(&firmware_path).await.map_err(|e| {
|
||||
log::error!(
|
||||
"Failed to read firmware file from {:?}: {}",
|
||||
firmware_path,
|
||||
e
|
||||
);
|
||||
AppError::InternalServerError
|
||||
})?;
|
||||
|
||||
if firmware_data.is_empty() {
|
||||
return Err(AppError::InternalServerError);
|
||||
}
|
||||
let output = BASE64_STANDARD.encode(firmware_data);
|
||||
Ok(FirmwareOutputResponse { output })
|
||||
}
|
||||
None => Err(AppError::NotFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
25
dollhouse/crates/dollhouse-backend/src/services/user.rs
Executable file
25
dollhouse/crates/dollhouse-backend/src/services/user.rs
Executable file
@@ -0,0 +1,25 @@
|
||||
use crate::{conversions::UserRoleConvert, utils::AppError};
|
||||
use dollhouse_api_types::UserResponse;
|
||||
use dollhouse_db::{Pool, repositories::UserRepository};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct UserService;
|
||||
|
||||
impl UserService {
|
||||
pub async fn get_user(pool: &Pool, user_id: Uuid) -> Result<UserResponse, AppError> {
|
||||
let mut conn = pool.get().await.map_err(|e| {
|
||||
log::error!("Some error with pool: {}", e);
|
||||
AppError::RepositoryError
|
||||
})?;
|
||||
match UserRepository::get_user(&mut conn, user_id).await {
|
||||
Ok(Some(user)) => Ok(UserResponse {
|
||||
id: user.id,
|
||||
role: user.role.to_api_role().unwrap(),
|
||||
username: user.username,
|
||||
corp_id: user.corp_id,
|
||||
}),
|
||||
Ok(None) => Err(AppError::NotFound),
|
||||
Err(e) => Err(AppError::RepositoryError),
|
||||
}
|
||||
}
|
||||
}
|
||||
114
dollhouse/crates/dollhouse-backend/src/utils.rs
Executable file
114
dollhouse/crates/dollhouse-backend/src/utils.rs
Executable file
@@ -0,0 +1,114 @@
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::error::ResponseError;
|
||||
use dollhouse_db::errors::DbError;
|
||||
use log::{debug, error};
|
||||
use serde_json;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PasswordError {
|
||||
#[error("Failed to verify password")]
|
||||
VerificationFailed,
|
||||
#[error("Failed to hash password: {0}")]
|
||||
HashError(String),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("Password error: {0}")]
|
||||
PasswordError(#[from] PasswordError),
|
||||
#[error("Database error")]
|
||||
RepositoryError,
|
||||
#[error("Not Found")]
|
||||
NotFound,
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
#[error("Internal server error")]
|
||||
InternalServerError,
|
||||
#[error("Invalid UUID format")]
|
||||
InvalidUuidFormat,
|
||||
#[error("Lua execution error: {0}")]
|
||||
LuaExecutionError(#[from] mlua::Error),
|
||||
#[error("Diesel error: {0}")]
|
||||
DieselError(#[from] DbError),
|
||||
#[error("Bad Request: {0}")]
|
||||
BadRequest(String),
|
||||
#[error("Multipart form error: {0}")]
|
||||
MultipartError(String),
|
||||
}
|
||||
|
||||
impl ResponseError for AppError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match self {
|
||||
AppError::RepositoryError => {
|
||||
error!("Database error");
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({
|
||||
"error": "Database error occurred",
|
||||
"message": "An error occurred while accessing the database"
|
||||
}))
|
||||
}
|
||||
AppError::PasswordError(e) => {
|
||||
error!("Password error: {}", e);
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({
|
||||
"error": "Authentication error",
|
||||
"message": "An error occurred during authentication"
|
||||
}))
|
||||
}
|
||||
AppError::NotFound => {
|
||||
debug!("Resource not found");
|
||||
HttpResponse::NotFound().json(serde_json::json!({
|
||||
"error": "Not found",
|
||||
"message": "The requested resource was not found"
|
||||
}))
|
||||
}
|
||||
AppError::Unauthorized => {
|
||||
debug!("Unauthorized access attempt");
|
||||
HttpResponse::Unauthorized().json(serde_json::json!({
|
||||
"error": "Unauthorized",
|
||||
"message": "Access denied"
|
||||
}))
|
||||
}
|
||||
AppError::InternalServerError => {
|
||||
error!("Internal server error");
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({
|
||||
"error": "Internal server error",
|
||||
"message": "An unexpected error occurred"
|
||||
}))
|
||||
}
|
||||
AppError::InvalidUuidFormat => {
|
||||
error!("Invalid UUID format");
|
||||
HttpResponse::BadRequest().json(serde_json::json!({
|
||||
"error": "Invalid UUID format",
|
||||
}))
|
||||
}
|
||||
AppError::LuaExecutionError(err) => {
|
||||
error!("Lua execution error: {}", err.to_string());
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({
|
||||
"error": "Lua execution error",
|
||||
"message": err.to_string()
|
||||
}))
|
||||
}
|
||||
AppError::DieselError(_) => {
|
||||
error!("Diesel error");
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({
|
||||
"error": "Diesel error",
|
||||
"message": "An error occurred during Diesel execution"
|
||||
}))
|
||||
}
|
||||
AppError::BadRequest(msg) => {
|
||||
error!("Bad Request: {}", msg);
|
||||
HttpResponse::BadRequest().json(serde_json::json!({
|
||||
"error": "Bad Request",
|
||||
"message": msg
|
||||
}))
|
||||
}
|
||||
AppError::MultipartError(msg) => {
|
||||
error!("Multipart error: {}", msg);
|
||||
HttpResponse::BadRequest().json(serde_json::json!({
|
||||
"error": "Multipart error",
|
||||
"message": msg
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user