This commit is contained in:
root
2025-12-14 10:39:18 +03:00
commit 639f4e2b4e
179 changed files with 21065 additions and 0 deletions

View 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"

View 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"),
}
}
}

View 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),
}
}

View 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
}

View 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))
}
}
}
}

View 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),
}
}
}

View 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),
}
}
}

View 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::*;

View 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),
}
}
}

View 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),
}
}
}

View 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
}))
}
}
}
}