From 698f01ab9c5ead9b152f9b7a97dbf49001e5b11e Mon Sep 17 00:00:00 2001 From: alyssa Date: Sun, 21 Dec 2025 17:24:04 -0500 Subject: [PATCH] most of a dash views api impl --- crates/api/src/endpoints/private.rs | 113 +++++++++++++++++++++++++- crates/api/src/endpoints/system.rs | 29 ++++++- crates/api/src/main.rs | 1 + crates/migrate/data/migrations/55.sql | 27 ++++++ crates/models/src/system_config.rs | 11 +++ 5 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 crates/migrate/data/migrations/55.sql diff --git a/crates/api/src/endpoints/private.rs b/crates/api/src/endpoints/private.rs index cbc7cf41..7131c0c7 100644 --- a/crates/api/src/endpoints/private.rs +++ b/crates/api/src/endpoints/private.rs @@ -1,10 +1,11 @@ -use crate::ApiContext; -use axum::{extract::State, response::Json}; +use crate::{ApiContext, auth::AuthState, error::fail}; +use axum::{Extension, extract::State, response::Json}; use fred::interfaces::*; use libpk::state::ShardState; use pk_macros::api_endpoint; use serde::Deserialize; use serde_json::{Value, json}; +use sqlx::Postgres; use std::collections::HashMap; #[allow(dead_code)] @@ -53,7 +54,7 @@ use axum::{ }; use hyper::StatusCode; use libpk::config; -use pluralkit_models::{PKSystem, PKSystemConfig, PrivacyLevel}; +use pluralkit_models::{PKDashView, PKSystem, PKSystemConfig, PrivacyLevel}; use reqwest::ClientBuilder; #[derive(serde::Deserialize, Debug)] @@ -187,3 +188,109 @@ pub async fn discord_callback( ) .into_response() } + +#[derive(serde::Deserialize, Debug)] +#[serde(tag = "action", rename_all = "snake_case")] +pub enum DashViewRequest { + Add { + name: String, + value: String, + }, + Patch { + id: String, + name: Option, + value: Option, + }, + Remove { id: String }, +} + +#[api_endpoint] +pub async fn dash_views( + Extension(auth): Extension, + State(ctx): State, + extract::Json(body): extract::Json, +) -> Json { + let Some(system_id) = auth.system_id() else { + return Err(crate::error::GENERIC_AUTH_ERROR); + }; + + match body { + DashViewRequest::Add { name, value } => { + match sqlx::query_as::( + "select * from dash_views where name = $1 and system = $2", + ) + .bind(&name) + .bind(system_id) + .fetch_optional(&ctx.db) + .await + { + Ok(val) => { + if val.is_some() { + return Err(crate::error::GENERIC_BAD_REQUEST); + }; + + match sqlx::query_as::( + "insert into dash_views (system, name, value) values ($1, $2, $3) returning *", + ) + .bind(system_id) + .bind(name) + .bind(value) + .fetch_one(&ctx.db) + .await + { + Ok(res) => Ok(Json(res.to_json())), + Err(err) => fail!(?err, "failed to insert dash views"), + } + } + Err(err) => fail!(?err, "failed to query dash views"), + } + } + DashViewRequest::Patch { id, name, value } => { + match sqlx::query_as::( + "select * from dash_views where id = $1 and system = $2", + ) + .bind(id) + .bind(system_id) + .fetch_optional(&ctx.db) + .await + { + Ok(val) => { + let Some(val) = val else { + return Err(crate::error::GENERIC_BAD_REQUEST); + }; + // update + Ok(Json(Value::Null)) + } + Err(err) => fail!(?err, "failed to query dash views"), + } + } + DashViewRequest::Remove { id } => { + match sqlx::query_as::( + "select * from dash_views where id = $1 and system = $2", + ) + .bind(id) + .bind(system_id) + .fetch_optional(&ctx.db) + .await + { + Ok(val) => { + let Some(val) = val else { + return Err(crate::error::GENERIC_BAD_REQUEST); + }; + match sqlx::query::( + "delete from dash_views where id = $1 and system = $2 returning *", + ) + .bind(val.id) + .bind(system_id) + .fetch_one(&ctx.db) + .await + { + Ok(_) => Ok(Json(Value::Null)), + Err(err) => fail!(?err, "failed to remove dash views"), + } + } + Err(err) => fail!(?err, "failed to query dash views"), + } + } + } +} diff --git a/crates/api/src/endpoints/system.rs b/crates/api/src/endpoints/system.rs index 58c9a154..58be13a9 100644 --- a/crates/api/src/endpoints/system.rs +++ b/crates/api/src/endpoints/system.rs @@ -3,7 +3,7 @@ use pk_macros::api_endpoint; use serde_json::{Value, json}; use sqlx::Postgres; -use pluralkit_models::{PKSystem, PKSystemConfig, PrivacyLevel}; +use pluralkit_models::{PKDashView, PKSystem, PKSystemConfig, PrivacyLevel}; use crate::{ApiContext, auth::AuthState, error::fail}; @@ -36,7 +36,32 @@ pub async fn get_system_settings( } Ok(Json(match access_level { - PrivacyLevel::Private => config.to_json(), + PrivacyLevel::Private => { + let mut config_json = config.clone().to_json(); + + match sqlx::query_as::( + "select * from dash_views where system = $1", + ) + .bind(system.id) + .fetch_all(&ctx.db) + .await + { + Ok(val) => { + config_json.as_object_mut().unwrap().insert( + "dash_views".to_string(), + serde_json::to_value( + &val.iter() + .map(|v| v.clone().to_json()) + .collect::>(), + ) + .unwrap(), + ); + } + Err(err) => fail!(?err, "failed to query dash views"), + }; + + config_json + } PrivacyLevel::Public => json!({ "pings_enabled": config.pings_enabled, "latch_timeout": config.latch_timeout, diff --git a/crates/api/src/main.rs b/crates/api/src/main.rs index a29f21ad..f22af0b0 100644 --- a/crates/api/src/main.rs +++ b/crates/api/src/main.rs @@ -123,6 +123,7 @@ fn router(ctx: ApiContext) -> Router { .route("/private/discord/callback", post(rproxy)) .route("/private/discord/callback2", post(endpoints::private::discord_callback)) .route("/private/discord/shard_state", get(endpoints::private::discord_state)) + .route("/private/dash_views", post(endpoints::private::dash_views)) .route("/private/stats", get(endpoints::private::meta)) .route("/v2/systems/{system_id}/oembed.json", get(rproxy)) diff --git a/crates/migrate/data/migrations/55.sql b/crates/migrate/data/migrations/55.sql new file mode 100644 index 00000000..dd10d842 --- /dev/null +++ b/crates/migrate/data/migrations/55.sql @@ -0,0 +1,27 @@ +-- database version 55 +-- dashboard views + +create function generate_dash_view_id_inner() returns char(10) as $$ + select string_agg(substr('aieu234567890', ceil(random() * 13)::integer, 1), '') from generate_series(1, 10) +$$ language sql volatile; + + +create function generate_dash_view_id() returns char(10) as $$ +declare newid char(10); +begin + loop + newid := generate_dash_view_id_inner(); + if not exists (select 1 from dash_views where id = newid) then return newid; end if; + end loop; +end +$$ language plpgsql volatile; + +create table dash_views ( + id text not null primary key default generate_dash_view_id(), + system int references systems(id) on delete cascade, + name text not null, + value text not null, + unique (system, name) +); + +update info set schema_version = 55; diff --git a/crates/models/src/system_config.rs b/crates/models/src/system_config.rs index 08302d2e..1d540c36 100644 --- a/crates/models/src/system_config.rs +++ b/crates/models/src/system_config.rs @@ -93,3 +93,14 @@ struct SystemConfig { #[json = "premium_lifetime"] premium_lifetime: bool } + +#[pk_model] +struct DashView { + #[json = "id"] + id: String, + system: SystemId, + #[json = "name"] + name: String, + #[json = "value"] + value: String +}