Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sandcrate-backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ tower-http = { version = "0.5", features = ["cors"] }
axum-extra = { version = "0.9", features = ["typed-header"] }
tokio-tungstenite = "0.21"
futures-util = { version = "0.3", features = ["sink"] }
uuid = { version = "1.0", features = ["v4"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid", "migrate"] }
dotenv = "0.15"
async-trait = "0.1"
26 changes: 13 additions & 13 deletions sandcrate-backend/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ use axum_extra::{
headers::{authorization::Bearer, Authorization},
};
use serde::{Serialize, Deserialize};
use std::sync::Arc;
use std::fs;
use std::path::Path as FsPath;

use crate::auth::{AuthConfig, validate_token};
use crate::auth::validate_token;
use crate::plugin;
use crate::AppState;

#[derive(Serialize)]
struct Plugin {
Expand Down Expand Up @@ -55,10 +55,10 @@ struct ApiResponse<T> {


async fn get_plugins(
State(config): State<Arc<AuthConfig>>,
State(state): State<AppState>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
) -> Result<Json<ApiResponse<PluginList>>, (StatusCode, Json<ApiResponse<PluginList>>)> {
let _user = validate_token(State(config.clone()), TypedHeader(Authorization(bearer))).await
let _user = validate_token(State(state.auth_config.clone()), TypedHeader(Authorization(bearer))).await
.map_err(|_| {
(
StatusCode::UNAUTHORIZED,
Expand Down Expand Up @@ -121,11 +121,11 @@ async fn get_plugins(
}

async fn get_plugin(
State(config): State<Arc<AuthConfig>>,
State(state): State<AppState>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
Path(plugin_id): Path<String>,
) -> Result<Response, (StatusCode, Json<ApiResponse<Plugin>>)> {
let _user = validate_token(State(config.clone()), TypedHeader(Authorization(bearer))).await
let _user = validate_token(State(state.auth_config.clone()), TypedHeader(Authorization(bearer))).await
.map_err(|_| {
(
StatusCode::UNAUTHORIZED,
Expand Down Expand Up @@ -200,12 +200,12 @@ async fn get_plugin(
}

async fn execute_plugin(
State(config): State<Arc<AuthConfig>>,
State(state): State<AppState>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
Path(plugin_id): Path<String>,
Json(request): Json<PluginExecutionRequest>,
) -> Result<Response, (StatusCode, Json<ApiResponse<PluginExecutionResponse>>)> {
let _user = validate_token(State(config.clone()), TypedHeader(Authorization(bearer))).await
let _user = validate_token(State(state.auth_config.clone()), TypedHeader(Authorization(bearer))).await
.map_err(|_| {
(
StatusCode::UNAUTHORIZED,
Expand Down Expand Up @@ -284,11 +284,11 @@ async fn execute_plugin(
}

async fn upload_plugin(
State(config): State<Arc<AuthConfig>>,
State(state): State<AppState>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
mut multipart: Multipart,
) -> Result<Json<ApiResponse<String>>, (StatusCode, Json<ApiResponse<String>>)> {
let _user = validate_token(State(config.clone()), TypedHeader(Authorization(bearer))).await
let _user = validate_token(State(state.auth_config.clone()), TypedHeader(Authorization(bearer))).await
.map_err(|_| {
(
StatusCode::UNAUTHORIZED,
Expand Down Expand Up @@ -356,11 +356,11 @@ async fn upload_plugin(
}

async fn delete_plugin(
State(config): State<Arc<AuthConfig>>,
State(state): State<AppState>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
Path(plugin_id): Path<String>,
) -> Result<Json<ApiResponse<String>>, (StatusCode, Json<ApiResponse<String>>)> {
let _user = validate_token(State(config.clone()), TypedHeader(Authorization(bearer))).await
let _user = validate_token(State(state.auth_config.clone()), TypedHeader(Authorization(bearer))).await
.map_err(|_| {
(
StatusCode::UNAUTHORIZED,
Expand Down Expand Up @@ -404,7 +404,7 @@ async fn delete_plugin(
}))
}

pub fn routes() -> Router<Arc<AuthConfig>> {
pub fn routes() -> Router<AppState> {
Router::new()
.route("/plugins", get(get_plugins))
.route("/plugins/upload", post(upload_plugin))
Expand Down
2 changes: 1 addition & 1 deletion sandcrate-backend/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub struct AuthConfig {
impl AuthConfig {
pub fn new() -> Self {
Self {
jwt_secret: "your-secret-key-change-in-production".to_string(),
jwt_secret: "testkey".to_string(),
}
}
}
Expand Down
201 changes: 104 additions & 97 deletions sandcrate-backend/src/database.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use sqlx::{PgPool, PgPoolOptions, postgres::PgPoolOptions as PgPoolOptionsPostgres};
use sqlx::PgPool;
use sqlx::postgres::PgPoolOptions;
use std::time::Duration;
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
Expand Down Expand Up @@ -32,7 +33,6 @@ pub async fn create_pool(config: &DatabaseConfig) -> Result<PgPool, sqlx::Error>
PgPoolOptions::new()
.max_connections(config.max_connections)
.min_connections(config.min_connections)
.connect_timeout(config.connect_timeout)
.idle_timeout(config.idle_timeout)
.max_lifetime(config.max_lifetime)
.connect(&config.url)
Expand Down Expand Up @@ -171,124 +171,135 @@ impl PluginRepository for PostgresPluginRepository {
let id = Uuid::new_v4();
let now = Utc::now();

sqlx::query_as!(
Plugin,
sqlx::query_as::<_, Plugin>(
r#"
INSERT INTO plugins (id, name, filename, file_path, file_size, description, version, author, tags, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING *
"#,
id,
plugin.name,
plugin.filename,
plugin.file_path,
plugin.file_size,
plugin.description,
plugin.version,
plugin.author,
&plugin.tags,
PluginStatus::Active as PluginStatus,
now,
now
"#
)
.bind(id)
.bind(plugin.name)
.bind(plugin.filename)
.bind(plugin.file_path)
.bind(plugin.file_size)
.bind(plugin.description)
.bind(plugin.version)
.bind(plugin.author)
.bind(&plugin.tags)
.bind(PluginStatus::Active)
.bind(now)
.bind(now)
.fetch_one(&self.pool)
.await
}

async fn get_plugin_by_id(&self, id: Uuid) -> Result<Option<Plugin>, sqlx::Error> {
sqlx::query_as!(
Plugin,
"SELECT * FROM plugins WHERE id = $1",
id
)
.fetch_optional(&self.pool)
.await
sqlx::query_as::<_, Plugin>("SELECT * FROM plugins WHERE id = $1")
.bind(id)
.fetch_optional(&self.pool)
.await
}

async fn get_plugin_by_filename(&self, filename: &str) -> Result<Option<Plugin>, sqlx::Error> {
sqlx::query_as!(
Plugin,
"SELECT * FROM plugins WHERE filename = $1",
filename
)
.fetch_optional(&self.pool)
.await
sqlx::query_as::<_, Plugin>("SELECT * FROM plugins WHERE filename = $1")
.bind(filename)
.fetch_optional(&self.pool)
.await
}

async fn list_plugins(&self, limit: Option<i64>, offset: Option<i64>) -> Result<Vec<Plugin>, sqlx::Error> {
let limit = limit.unwrap_or(100);
let offset = offset.unwrap_or(0);

sqlx::query_as!(
Plugin,
"SELECT * FROM plugins ORDER BY created_at DESC LIMIT $1 OFFSET $2",
limit,
offset
)
.fetch_all(&self.pool)
.await
sqlx::query_as::<_, Plugin>("SELECT * FROM plugins ORDER BY created_at DESC LIMIT $1 OFFSET $2")
.bind(limit)
.bind(offset)
.fetch_all(&self.pool)
.await
}

async fn update_plugin(&self, id: Uuid, updates: UpdatePluginRequest) -> Result<Plugin, sqlx::Error> {
let now = Utc::now();

let mut query = String::from("UPDATE plugins SET updated_at = $1");
let mut params: Vec<Box<dyn sqlx::Encode<'_, sqlx::Postgres> + Send + Sync>> = vec![Box::new(now)];
// Build the query dynamically based on what fields are being updated
let mut query_parts = vec!["UPDATE plugins SET updated_at = $1".to_string()];
let mut param_count = 1;

if let Some(name) = updates.name {
if updates.name.is_some() {
param_count += 1;
query.push_str(&format!(", name = ${}", param_count));
params.push(Box::new(name));
let name_part = format!("name = ${}", param_count);
query_parts.push(name_part);
}

if let Some(description) = updates.description {
if updates.description.is_some() {
param_count += 1;
query.push_str(&format!(", description = ${}", param_count));
params.push(Box::new(description));
let desc_part = format!("description = ${}", param_count);
query_parts.push(desc_part);
}

if let Some(version) = updates.version {
if updates.version.is_some() {
param_count += 1;
query.push_str(&format!(", version = ${}", param_count));
params.push(Box::new(version));
let version_part = format!("version = ${}", param_count);
query_parts.push(version_part);
}

if let Some(author) = updates.author {
if updates.author.is_some() {
param_count += 1;
query.push_str(&format!(", author = ${}", param_count));
params.push(Box::new(author));
let author_part = format!("author = ${}", param_count);
query_parts.push(author_part);
}

if let Some(tags) = updates.tags {
if updates.tags.is_some() {
param_count += 1;
query.push_str(&format!(", tags = ${}", param_count));
params.push(Box::new(tags));
let tags_part = format!("tags = ${}", param_count);
query_parts.push(tags_part);
}

if let Some(status) = updates.status {
if updates.status.is_some() {
param_count += 1;
query.push_str(&format!(", status = ${}", param_count));
params.push(Box::new(status));
let status_part = format!("status = ${}", param_count);
query_parts.push(status_part);
}

param_count += 1;
query.push_str(&format!(" WHERE id = ${} RETURNING *", param_count));
params.push(Box::new(id));

sqlx::query_as::<_, Plugin>(&query)
.bind_all(params)
.fetch_one(&self.pool)
.await
let query = format!("{} WHERE id = ${} RETURNING *", query_parts.join(", "), param_count);

// Build the query with individual bind calls
let mut query_builder = sqlx::query_as::<_, Plugin>(&query).bind(now);

if let Some(name) = updates.name {
query_builder = query_builder.bind(name);
}

if let Some(description) = updates.description {
query_builder = query_builder.bind(description);
}

if let Some(version) = updates.version {
query_builder = query_builder.bind(version);
}

if let Some(author) = updates.author {
query_builder = query_builder.bind(author);
}

if let Some(tags) = updates.tags {
query_builder = query_builder.bind(tags);
}

if let Some(status) = updates.status {
query_builder = query_builder.bind(status);
}

query_builder.bind(id).fetch_one(&self.pool).await
}

async fn delete_plugin(&self, id: Uuid) -> Result<bool, sqlx::Error> {
let result = sqlx::query!(
"DELETE FROM plugins WHERE id = $1",
id
)
.execute(&self.pool)
.await?;
let result = sqlx::query("DELETE FROM plugins WHERE id = $1")
.bind(id)
.execute(&self.pool)
.await?;

Ok(result.rows_affected() > 0)
}
Expand All @@ -297,35 +308,31 @@ impl PluginRepository for PostgresPluginRepository {
let id = Uuid::new_v4();
let now = Utc::now();

sqlx::query_as!(
PluginExecution,
sqlx::query_as::<_, PluginExecution>(
r#"
INSERT INTO plugin_executions (id, plugin_id, user_id, session_id, parameters, status, started_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
"#,
id,
execution.plugin_id,
execution.user_id,
execution.session_id,
execution.parameters,
ExecutionStatus::Running as ExecutionStatus,
now
"#
)
.bind(id)
.bind(execution.plugin_id)
.bind(execution.user_id)
.bind(execution.session_id)
.bind(execution.parameters)
.bind(ExecutionStatus::Running)
.bind(now)
.fetch_one(&self.pool)
.await
}

async fn get_execution_history(&self, plugin_id: Uuid, limit: Option<i64>) -> Result<Vec<PluginExecution>, sqlx::Error> {
let limit = limit.unwrap_or(50);

sqlx::query_as!(
PluginExecution,
"SELECT * FROM plugin_executions WHERE plugin_id = $1 ORDER BY started_at DESC LIMIT $2",
plugin_id,
limit
)
.fetch_all(&self.pool)
.await
sqlx::query_as::<_, PluginExecution>("SELECT * FROM plugin_executions WHERE plugin_id = $1 ORDER BY started_at DESC LIMIT $2")
.bind(plugin_id)
.bind(limit)
.fetch_all(&self.pool)
.await
}
}
Loading
Loading