Webhook-endepunkt: POST /api/webhook/<token> → content-node (oppgave 29.4)
Nytt offentlig endepunkt som mottar vilkårlig JSON og oppretter en content-node i målsamlingen. Webhook-noder har et unikt token i metadata som brukes til autentisering i stedet for JWT. Flyten: token-oppslag → finn belongs_to-edge til samling → opprett content-node med payload i metadata → belongs_to-edge → tilgangspropagering fra samling. Trekker ut title/content fra payload automatisk når feltene finnes.
This commit is contained in:
parent
30dc76db8a
commit
8c77b60561
3 changed files with 167 additions and 2 deletions
|
|
@ -30,6 +30,7 @@ pub mod ws;
|
||||||
pub mod mixer;
|
pub mod mixer;
|
||||||
pub mod feed_poller;
|
pub mod feed_poller;
|
||||||
pub mod orchestration_trigger;
|
pub mod orchestration_trigger;
|
||||||
|
mod webhook;
|
||||||
pub mod script_compiler;
|
pub mod script_compiler;
|
||||||
pub mod script_executor;
|
pub mod script_executor;
|
||||||
pub mod tiptap;
|
pub mod tiptap;
|
||||||
|
|
@ -291,6 +292,8 @@ async fn main() {
|
||||||
.route("/intentions/set_mute", post(mixer::set_mute))
|
.route("/intentions/set_mute", post(mixer::set_mute))
|
||||||
.route("/intentions/toggle_effect", post(mixer::toggle_effect))
|
.route("/intentions/toggle_effect", post(mixer::toggle_effect))
|
||||||
.route("/intentions/set_mixer_role", post(mixer::set_mixer_role))
|
.route("/intentions/set_mixer_role", post(mixer::set_mixer_role))
|
||||||
|
// Webhook universell input (oppgave 29.4)
|
||||||
|
.route("/api/webhook/{token}", post(webhook::handle_webhook))
|
||||||
// Observerbarhet (oppgave 12.1)
|
// Observerbarhet (oppgave 12.1)
|
||||||
.route("/metrics", get(metrics::metrics_endpoint))
|
.route("/metrics", get(metrics::metrics_endpoint))
|
||||||
.layer(middleware::from_fn_with_state(state.clone(), metrics::latency_middleware))
|
.layer(middleware::from_fn_with_state(state.clone(), metrics::latency_middleware))
|
||||||
|
|
|
||||||
163
maskinrommet/src/webhook.rs
Normal file
163
maskinrommet/src/webhook.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
// Webhook-endepunkt: POST /api/webhook/<token>
|
||||||
|
//
|
||||||
|
// Offentlig endepunkt (ingen JWT). Validerer token mot en webhook-node
|
||||||
|
// i databasen, oppretter content-node med payload i metadata, og kobler
|
||||||
|
// den til målsamlingen via belongs_to-edge.
|
||||||
|
//
|
||||||
|
// Ref: docs/features/universell_input.md (Webhook-seksjon)
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct WebhookResponse {
|
||||||
|
pub node_id: Uuid,
|
||||||
|
pub edge_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_found(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
|
||||||
|
(
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
Json(ErrorResponse { error: msg.to_string() }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn internal_error(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(ErrorResponse { error: msg.to_string() }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rad fra token-oppslag: webhook-nodens id og dens målsamling.
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
struct WebhookLookup {
|
||||||
|
webhook_id: Uuid,
|
||||||
|
collection_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /api/webhook/<token>
|
||||||
|
///
|
||||||
|
/// Mottar vilkårlig JSON-body og oppretter en content-node med payload
|
||||||
|
/// i metadata. Noden knyttes til webhook-nodens målsamling via belongs_to-edge.
|
||||||
|
pub async fn handle_webhook(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(token): Path<Uuid>,
|
||||||
|
body: Option<Json<serde_json::Value>>,
|
||||||
|
) -> Result<Json<WebhookResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
|
// -- Slå opp webhook-node via token --
|
||||||
|
// Token lagres i metadata.token på en webhook-node.
|
||||||
|
// Målsamlingen finnes via belongs_to-edge fra webhook-noden.
|
||||||
|
let lookup = sqlx::query_as::<_, WebhookLookup>(
|
||||||
|
r#"SELECT w.id AS webhook_id, e.target_id AS collection_id
|
||||||
|
FROM nodes w
|
||||||
|
JOIN edges e ON e.source_id = w.id AND e.edge_type = 'belongs_to'
|
||||||
|
WHERE w.node_kind = 'webhook'
|
||||||
|
AND w.metadata->>'token' = $1::text"#,
|
||||||
|
)
|
||||||
|
.bind(token.to_string())
|
||||||
|
.fetch_optional(&state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("PG-feil ved webhook-token-oppslag: {e}");
|
||||||
|
internal_error("Databasefeil")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let lookup = match lookup {
|
||||||
|
Some(l) => l,
|
||||||
|
None => return Err(not_found("Ugyldig webhook-token")),
|
||||||
|
};
|
||||||
|
|
||||||
|
// -- Payload --
|
||||||
|
let payload = body
|
||||||
|
.map(|Json(v)| v)
|
||||||
|
.unwrap_or(serde_json::json!({}));
|
||||||
|
|
||||||
|
// Trekk ut tittel fra payload hvis den har et "title"-felt
|
||||||
|
let title = payload
|
||||||
|
.get("title")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Trekk ut innhold fra payload hvis den har et "content" eller "body"-felt
|
||||||
|
let content = payload
|
||||||
|
.get("content")
|
||||||
|
.or_else(|| payload.get("body"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// -- Opprett content-node --
|
||||||
|
let node_id = Uuid::now_v7();
|
||||||
|
let metadata = serde_json::json!({
|
||||||
|
"source": "webhook",
|
||||||
|
"webhook_id": lookup.webhook_id.to_string(),
|
||||||
|
"payload": payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
r#"INSERT INTO nodes (id, node_kind, title, content, visibility, metadata, created_by)
|
||||||
|
VALUES ($1, 'content', NULLIF($2, ''), NULLIF($3, ''), 'hidden'::visibility, $4, $5)"#,
|
||||||
|
)
|
||||||
|
.bind(node_id)
|
||||||
|
.bind(&title)
|
||||||
|
.bind(&content)
|
||||||
|
.bind(&metadata)
|
||||||
|
.bind(lookup.webhook_id)
|
||||||
|
.execute(&state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("PG-feil ved opprettelse av webhook-node: {e}");
|
||||||
|
internal_error("Databasefeil ved opprettelse av node")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// -- belongs_to-edge til målsamling --
|
||||||
|
let edge_id = Uuid::now_v7();
|
||||||
|
sqlx::query(
|
||||||
|
r#"INSERT INTO edges (id, source_id, target_id, edge_type, metadata, system, created_by)
|
||||||
|
VALUES ($1, $2, $3, 'belongs_to', '{}'::jsonb, true, $4)"#,
|
||||||
|
)
|
||||||
|
.bind(edge_id)
|
||||||
|
.bind(node_id)
|
||||||
|
.bind(lookup.collection_id)
|
||||||
|
.bind(lookup.webhook_id)
|
||||||
|
.execute(&state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("PG-feil ved opprettelse av belongs_to-edge: {e}");
|
||||||
|
internal_error("Databasefeil ved opprettelse av edge")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// -- Propager tilgang fra samlingen til den nye noden --
|
||||||
|
if let Err(e) = sqlx::query("SELECT propagate_belongs_to_access($1, $2)")
|
||||||
|
.bind(node_id)
|
||||||
|
.bind(lookup.collection_id)
|
||||||
|
.execute(&state.db)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!("propagate_belongs_to_access feilet for webhook-node: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
node_id = %node_id,
|
||||||
|
edge_id = %edge_id,
|
||||||
|
webhook_id = %lookup.webhook_id,
|
||||||
|
collection_id = %lookup.collection_id,
|
||||||
|
"Webhook mottatt — content-node opprettet"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Json(WebhookResponse { node_id, edge_id }))
|
||||||
|
}
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -397,8 +397,7 @@ noden er det som lever videre.
|
||||||
- [x] 29.3 Feed-orkestrering: standard-orkestrering "Overvåk RSS-feed" som bruker synops-feed. Konfigurerbar per samling. Nye artikler havner i innboks eller direkte i en kanal.
|
- [x] 29.3 Feed-orkestrering: standard-orkestrering "Overvåk RSS-feed" som bruker synops-feed. Konfigurerbar per samling. Nye artikler havner i innboks eller direkte i en kanal.
|
||||||
|
|
||||||
### Webhook (universell ekstern input)
|
### Webhook (universell ekstern input)
|
||||||
- [~] 29.4 Webhook-endepunkt i vaktmesteren: `POST /api/webhook/<token>` → opprett node fra JSON-body. Hvert webhook har et unikt token (UUID) knyttet til en `webhook`-node med `belongs_to`-edge til målsamling. Validerer token, oppretter `content`-node med payload i metadata.
|
- [x] 29.4 Webhook-endepunkt i vaktmesteren: `POST /api/webhook/<token>` → opprett node fra JSON-body. Hvert webhook har et unikt token (UUID) knyttet til en `webhook`-node med `belongs_to`-edge til målsamling. Validerer token, oppretter `content`-node med payload i metadata.
|
||||||
> Påbegynt: 2026-03-18T21:35
|
|
||||||
- [ ] 29.5 Webhook-admin: UI for å opprette/administrere webhooks. Vis token, mål-samling, siste hendelser, aktivitet-logg. Regenerer token. Deaktiver/slett.
|
- [ ] 29.5 Webhook-admin: UI for å opprette/administrere webhooks. Vis token, mål-samling, siste hendelser, aktivitet-logg. Regenerer token. Deaktiver/slett.
|
||||||
- [ ] 29.6 Webhook-templates: forhåndsdefinerte mappinger for kjente tjenester (GitHub → commits/issues, Slack → meldinger, CI/CD → build-status). Template mapper JSON-felt til node title/content/metadata.
|
- [ ] 29.6 Webhook-templates: forhåndsdefinerte mappinger for kjente tjenester (GitHub → commits/issues, Slack → meldinger, CI/CD → build-status). Template mapper JSON-felt til node title/content/metadata.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue