mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
feat: api key/basic/hmac auth for http triggers (#5476)
* feat: add webhook configuration in front and start handling logic in back * feat: add generic implementation * update: add stripe provider working on auth * merge main * handling new webhook, add generic implem * feat: add auth method and api key and handling custom authentication * nits * added generic implem for webhook provider * feat: added resource webhook_auth to handle http route auth, refactored backend code * update sqlx * fix: zoom and twitch challenge request event, call verify function * removing webhook name to make it more general, properly added custom auth option alongside windmill_auth option * update .sqlx * update deply to for http routes * update .sqlx * add different auth method for http routes * add custom script method and custom signature fixed bug with webhook auth * update auth options * update .sqlx * add new auth method * update .sqlx * refacto function and fix frontend bug * fix typo, update .sqlx and handle pototential error * nits * nits * add script template flow * Update frontend/src/lib/components/triggers/http/utils.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * nits * update: remove wrong trigger type and use right icon for sqs trigger * nits * update: .sqlx * update: reunite flow and script logic for script placeholder value replacement * Update backend/windmill-api/src/http_trigger_auth.rs Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> * delete vite config * remove darks mode observer * update: add new option to capture panel for http route * nits * revert * nits * typo --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> Co-authored-by: HugoCasa <hugo@casademont.ch>
This commit is contained in:
+25
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n requires_auth, \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
|
||||
"query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n authentication_method as \"authentication_method: _\", \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n authentication_resource_path,\n wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -83,8 +83,22 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "requires_auth",
|
||||
"type_info": "Bool"
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
@@ -98,11 +112,16 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "authentication_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "wrap_body",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 18,
|
||||
"name": "raw_string",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -130,9 +149,10 @@
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "157090e958a907a09554c2491cabf9a8108c436cba1d2b770e9104d1c8590bcb"
|
||||
"hash": "144e4eccfd1c1e729e3c864bd5dc3316248719dfa8a6c9e1d15a7931638e86db"
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n edited_at = now(), \n is_static_website = $16\n WHERE \n workspace_id = $17 AND \n path = $18\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "187e8f85a71dea958e89fdfdf96c913a19eef8678dc7890c2f0e1ef8758ec43b"
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n wrap_body,\n raw_string,\n script_path, \n is_flow, \n is_async, \n requires_auth, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, now(), $16\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "22888025706d457da65207ea9d592079c7274ca0827dc2a26b29394c12b3f7fc"
|
||||
}
|
||||
+33
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n path, \n script_path, \n is_flow, \n route_path, \n workspace_id, \n is_async, \n requires_auth, \n edited_by, \n email, \n static_asset_config AS \"static_asset_config: _\",\n wrap_body,\n raw_string,\n workspaced_route,\n is_static_website\n FROM \n http_trigger \n WHERE \n http_method = $1\n ",
|
||||
"query": "\n SELECT \n path, \n script_path, \n is_flow, \n route_path, \n authentication_resource_path,\n workspace_id, \n is_async, \n authentication_method AS \"authentication_method: _\", \n edited_by, \n email, \n static_asset_config AS \"static_asset_config: _\",\n wrap_body,\n raw_string,\n workspaced_route,\n is_static_website\n FROM \n http_trigger \n WHERE \n http_method = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -25,51 +25,70 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "workspace_id",
|
||||
"name": "authentication_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "requires_auth",
|
||||
"type_info": "Bool"
|
||||
"ordinal": 7,
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 11,
|
||||
"name": "wrap_body",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 12,
|
||||
"name": "raw_string",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 13,
|
||||
"name": "workspaced_route",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 14,
|
||||
"name": "is_static_website",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -97,6 +116,7 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -109,5 +129,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "54798bea6f75ab6d3c680b1085f0462df32172ad0d67321495c94bf2d59010f5"
|
||||
"hash": "4053f0bb30f651ddf2214115748daca0ea457da8252394eeeead0897d184f6da"
|
||||
}
|
||||
+36
-16
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n script_path, \n is_flow, \n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n requires_auth, \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ",
|
||||
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -30,46 +30,65 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "script_path",
|
||||
"name": "authentication_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 11,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 12,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "requires_auth",
|
||||
"type_info": "Bool"
|
||||
"ordinal": 13,
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 14,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
@@ -87,22 +106,22 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 15,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 16,
|
||||
"name": "is_static_website",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 17,
|
||||
"name": "wrap_body",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 18,
|
||||
"name": "raw_string",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
@@ -118,6 +137,7 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -133,5 +153,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5824982a58edd5bb2666daac49b2a6ff8553b9c196ca64ea2752df8cfe30c30c"
|
||||
"hash": "56c2522a12f91515e38290e4680a55a4727195125cd49a2f92f89bcdf74dc364"
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n http_trigger \n SET \n script_path = $1, \n path = $2, \n is_flow = $3, \n http_method = $4,\n static_asset_config = $5, \n edited_by = $6, \n email = $7, \n is_async = $8, \n requires_auth = $9, \n edited_at = now(), \n is_static_website = $10\n WHERE \n workspace_id = $11 AND \n path = $12\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5d3401f608b27937285e418f7bd64bd112977c995bf266680c54f58ad57fd4b6"
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), $17\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8c30e91c2486f7511563621e7e805d0588a9ec8bbea9db10e95783e27e35bc12"
|
||||
}
|
||||
+25
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n path, \n script_path, \n is_flow, \n route_path, \n workspace_id, \n is_async, \n requires_auth, \n edited_by, \n email,\n static_asset_config AS \"static_asset_config: _\",\n wrap_body,\n raw_string,\n workspaced_route,\n is_static_website \n FROM \n http_trigger \n WHERE \n workspace_id = $1 AND \n http_method = $2\n ",
|
||||
"query": "\n SELECT \n path, \n script_path, \n is_flow, \n route_path, \n workspace_id, \n is_async, \n authentication_method AS \"authentication_method: _\", \n edited_by, \n email,\n static_asset_config AS \"static_asset_config: _\",\n wrap_body,\n raw_string,\n workspaced_route,\n is_static_website,\n authentication_resource_path\n FROM \n http_trigger \n WHERE \n workspace_id = $1 AND \n http_method = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -35,8 +35,22 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "requires_auth",
|
||||
"type_info": "Bool"
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
@@ -72,6 +86,11 @@
|
||||
"ordinal": 13,
|
||||
"name": "is_static_website",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "authentication_resource_path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -107,8 +126,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6860deb7064e57b9e7646438145dc57cc51c7ea256058daf170a1ec8573ed102"
|
||||
"hash": "927149213e0f8ae983652ef80464f646d6be80e702193f1acdd40dd6033652e4"
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n http_trigger \n SET \n workspaced_route = $1,\n wrap_body = $2,\n raw_string = $3,\n authentication_resource_path = $4,\n script_path = $5, \n path = $6, \n is_flow = $7, \n http_method = $8, \n static_asset_config = $9, \n edited_by = $10, \n email = $11, \n is_async = $12, \n authentication_method = $13, \n edited_at = now(), \n is_static_website = $14\n WHERE \n workspace_id = $15 AND \n path = $16\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "dd06bdc09968add6a7c09f124f1f9b717990371d98e0784e96eb31cfdd17885a"
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ddf2eccb78a310ed00c7d8b9c3f05d394a7cbcf0038c72a78add5c7b02ef5927"
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3, \n wrap_body = $4,\n raw_string = $5,\n script_path = $6, \n path = $7, \n is_flow = $8, \n http_method = $9, \n static_asset_config = $10, \n edited_by = $11, \n email = $12, \n is_async = $13, \n requires_auth = $14, \n edited_at = now(), \n is_static_website = $15\n WHERE \n workspace_id = $16 AND \n path = $17\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "fd724083c4ef6ec0f1cede5813c89b3abe9d04b86ad50c0f62903a1a79479a60"
|
||||
}
|
||||
Generated
+4
@@ -13652,6 +13652,7 @@ dependencies = [
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"constant_time_eq",
|
||||
"deno_core",
|
||||
"dotenv",
|
||||
"futures",
|
||||
@@ -13668,6 +13669,7 @@ dependencies = [
|
||||
"rustls 0.23.25",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2 0.10.8",
|
||||
"sqlx",
|
||||
"tikv-jemalloc-ctl",
|
||||
@@ -13712,6 +13714,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz 0.10.1",
|
||||
"const_format",
|
||||
"constant_time_eq",
|
||||
"cookie 0.17.0",
|
||||
"cron",
|
||||
"datafusion",
|
||||
@@ -13752,6 +13755,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sha1",
|
||||
"sha2 0.10.8",
|
||||
"sql-builder",
|
||||
"sqlx",
|
||||
|
||||
@@ -118,6 +118,8 @@ serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
deno_core = { workspace = true, optional = true }
|
||||
object_store = { workspace = true, optional = true }
|
||||
sha1 = { workspace = true, optional = true }
|
||||
constant_time_eq = { workspace = true, optional = true }
|
||||
quote.workspace = true
|
||||
memchr.workspace = true
|
||||
v8 = { workspace = true, optional = true }
|
||||
@@ -248,6 +250,7 @@ base64 = "^0"
|
||||
base32 = "^0"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.6"
|
||||
sha1 = "0.10.6"
|
||||
sqlx = { version = "0.8.0", features = [
|
||||
"macros",
|
||||
"migrate",
|
||||
@@ -266,6 +269,7 @@ futures-core = "^0"
|
||||
lazy_static = "1.4.0"
|
||||
serde_derive = "1.0.147"
|
||||
const_format = { version = "0.2", features = ["rust_1_64", "rust_1_51"] }
|
||||
constant_time_eq = "0.3.1"
|
||||
dyn-iter = "0.2.0"
|
||||
rsa = "^0"
|
||||
async_zip = { version = "0.0.17", features = ["tokio", "tokio-fs", "deflate", "chrono"] }
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
ALTER TABLE http_trigger
|
||||
DROP COLUMN authentication_resource_path,
|
||||
ALTER COLUMN authentication_method DROP DEFAULT,
|
||||
ALTER COLUMN authentication_method TYPE boolean
|
||||
USING CASE
|
||||
WHEN authentication_method = 'windmill'::AUTHENTICATION_METHOD THEN true
|
||||
ELSE false
|
||||
END,
|
||||
ALTER COLUMN authentication_method SET NOT NULL,
|
||||
ALTER COLUMN authentication_method SET DEFAULT false;
|
||||
|
||||
ALTER TABLE http_trigger
|
||||
RENAME COLUMN authentication_method TO requires_auth;
|
||||
|
||||
DROP TYPE AUTHENTICATION_METHOD;
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TYPE AUTHENTICATION_METHOD AS ENUM (
|
||||
'none',
|
||||
'windmill',
|
||||
'api_key',
|
||||
'basic_http',
|
||||
'custom_script',
|
||||
'signature'
|
||||
);
|
||||
|
||||
ALTER TABLE http_trigger
|
||||
RENAME COLUMN requires_auth TO authentication_method;
|
||||
|
||||
ALTER TABLE http_trigger
|
||||
ADD COLUMN authentication_resource_path VARCHAR(255) DEFAULT NULL,
|
||||
ALTER COLUMN authentication_method DROP DEFAULT,
|
||||
ALTER COLUMN authentication_method TYPE AUTHENTICATION_METHOD
|
||||
USING CASE
|
||||
WHEN authentication_method = true THEN 'windmill'::AUTHENTICATION_METHOD
|
||||
ELSE 'none'::AUTHENTICATION_METHOD
|
||||
END,
|
||||
ALTER COLUMN authentication_method SET NOT NULL,
|
||||
ALTER COLUMN authentication_method SET DEFAULT 'none'::AUTHENTICATION_METHOD;
|
||||
@@ -26,7 +26,7 @@ smtp = ["dep:mail-parser", "dep:openssl", "windmill-common/smtp"]
|
||||
license = ["dep:rsa"]
|
||||
zip = ["dep:async_zip"]
|
||||
oauth2 = ["dep:async-oauth2"]
|
||||
http_trigger = ["dep:matchit"]
|
||||
http_trigger = ["dep:matchit", "dep:thiserror", "dep:sha1", "dep:constant_time_eq"]
|
||||
static_frontend = ["dep:rust-embed"]
|
||||
postgres_trigger = ["dep:rust-postgres", "dep:pg_escape", "dep:byteorder", "dep:thiserror", "dep:rust_decimal", "dep:rust-postgres-native-tls"]
|
||||
mqtt_trigger = ["dep:thiserror", "dep:rumqttc"]
|
||||
@@ -85,6 +85,8 @@ tokio-tar.workspace = true
|
||||
hmac.workspace = true
|
||||
cookie.workspace = true
|
||||
sha2.workspace = true
|
||||
sha1 = { workspace = true, optional = true }
|
||||
constant_time_eq = { workspace = true, optional = true }
|
||||
urlencoding.workspace = true
|
||||
lazy_static.workspace = true
|
||||
prometheus = { workspace = true, optional = true }
|
||||
|
||||
@@ -13939,6 +13939,17 @@ components:
|
||||
- edited_at
|
||||
- is_flow
|
||||
|
||||
|
||||
AuthenticationMethod:
|
||||
type: string
|
||||
enum:
|
||||
- none
|
||||
- windmill
|
||||
- api_key
|
||||
- basic_http
|
||||
- custom_script
|
||||
- signature
|
||||
|
||||
HttpTrigger:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/TriggerExtraProperty"
|
||||
@@ -13965,10 +13976,12 @@ components:
|
||||
- put
|
||||
- delete
|
||||
- patch
|
||||
authentication_resource_path:
|
||||
type: string
|
||||
is_async:
|
||||
type: boolean
|
||||
requires_auth:
|
||||
type: boolean
|
||||
authentication_method:
|
||||
$ref: "#/components/schemas/AuthenticationMethod"
|
||||
is_static_website:
|
||||
type: boolean
|
||||
workspaced_route:
|
||||
@@ -13981,7 +13994,7 @@ components:
|
||||
required:
|
||||
- route_path
|
||||
- is_async
|
||||
- requires_auth
|
||||
- authentication_method
|
||||
- http_method
|
||||
- is_static_website
|
||||
- workspaced_route
|
||||
@@ -14020,10 +14033,12 @@ components:
|
||||
- put
|
||||
- delete
|
||||
- patch
|
||||
authentication_resource_path:
|
||||
type: string
|
||||
is_async:
|
||||
type: boolean
|
||||
requires_auth:
|
||||
type: boolean
|
||||
authentication_method:
|
||||
$ref: "#/components/schemas/AuthenticationMethod"
|
||||
is_static_website:
|
||||
type: boolean
|
||||
wrap_body:
|
||||
@@ -14037,7 +14052,7 @@ components:
|
||||
- route_path
|
||||
- is_flow
|
||||
- is_async
|
||||
- requires_auth
|
||||
- authentication_method
|
||||
- http_method
|
||||
- is_static_website
|
||||
|
||||
@@ -14063,6 +14078,8 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- s3
|
||||
authentication_resource_path:
|
||||
type: string
|
||||
is_flow:
|
||||
type: boolean
|
||||
http_method:
|
||||
@@ -14075,8 +14092,8 @@ components:
|
||||
- patch
|
||||
is_async:
|
||||
type: boolean
|
||||
requires_auth:
|
||||
type: boolean
|
||||
authentication_method:
|
||||
$ref: "#/components/schemas/AuthenticationMethod"
|
||||
is_static_website:
|
||||
type: boolean
|
||||
wrap_body:
|
||||
@@ -14089,7 +14106,7 @@ components:
|
||||
- is_flow
|
||||
- kind
|
||||
- is_async
|
||||
- requires_auth
|
||||
- authentication_method
|
||||
- http_method
|
||||
- is_static_website
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ use crate::postgres_triggers::{
|
||||
generate_random_string, get_database_connection, PublicationData,
|
||||
};
|
||||
#[cfg(feature = "http_trigger")]
|
||||
use axum::extract::Request;
|
||||
#[cfg(feature = "http_trigger")]
|
||||
use axum::response::IntoResponse;
|
||||
#[cfg(feature = "http_trigger")]
|
||||
use http::HeaderMap;
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
use itertools::Itertools;
|
||||
@@ -99,6 +103,8 @@ pub fn workspaced_unauthed_service() -> Router {
|
||||
struct HttpTriggerConfig {
|
||||
route_path: String,
|
||||
http_method: HttpMethod,
|
||||
raw_string: Option<bool>,
|
||||
wrap_body: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "kafka"))]
|
||||
@@ -643,8 +649,12 @@ async fn http_payload(
|
||||
Query(query): Query<HashMap<String, String>>,
|
||||
method: http::Method,
|
||||
headers: HeaderMap,
|
||||
args: WebhookArgs,
|
||||
) -> Result<StatusCode> {
|
||||
request: Request,
|
||||
) -> std::result::Result<StatusCode, impl IntoResponse> {
|
||||
use axum::response::Response;
|
||||
|
||||
use crate::args::try_from_request_body;
|
||||
|
||||
let route_path = route_path.to_path();
|
||||
let path = path.replace(".", "/");
|
||||
|
||||
@@ -656,16 +666,32 @@ async fn http_payload(
|
||||
matches!(kind, RunnableKind::Flow),
|
||||
&TriggerKind::Http,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
let authed = fetch_api_authed(owner.clone(), email, &w_id, &db, None).await?;
|
||||
let args = args.to_push_args_owned(&authed, &db, &w_id).await?;
|
||||
let args = try_from_request_body(
|
||||
request,
|
||||
&db,
|
||||
http_trigger_config.raw_string,
|
||||
http_trigger_config.wrap_body,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
let authed = fetch_api_authed(owner.clone(), email, &w_id, &db, None)
|
||||
.await
|
||||
.map_err(|e| e.into_response())?;
|
||||
let mut args = args
|
||||
.to_push_args_owned(&authed, &db, &w_id)
|
||||
.await
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
let mut router = matchit::Router::new();
|
||||
router.insert(&http_trigger_config.route_path, ()).ok();
|
||||
let match_ = router.at(route_path).ok();
|
||||
|
||||
let match_ = not_found_if_none(match_, "capture http trigger", &route_path)?;
|
||||
let match_ = not_found_if_none(match_, "capture http trigger", &route_path)
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
let matchit::Match { params, .. } = match_;
|
||||
|
||||
@@ -674,7 +700,9 @@ async fn http_payload(
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
|
||||
let extra: HashMap<String, Box<RawValue>> = HashMap::from_iter(vec![(
|
||||
let extra = args.extra.get_or_insert_with(HashMap::new);
|
||||
|
||||
extra.insert(
|
||||
"wm_trigger".to_string(),
|
||||
build_http_trigger_extra(
|
||||
&http_trigger_config.route_path,
|
||||
@@ -685,8 +713,10 @@ async fn http_payload(
|
||||
&headers,
|
||||
)
|
||||
.await,
|
||||
)]);
|
||||
);
|
||||
|
||||
let extra = Some(to_raw_value(&extra));
|
||||
args.extra = None;
|
||||
insert_capture_payload(
|
||||
&db,
|
||||
&w_id,
|
||||
@@ -694,10 +724,11 @@ async fn http_payload(
|
||||
matches!(kind, RunnableKind::Flow),
|
||||
&TriggerKind::Http,
|
||||
args,
|
||||
Some(to_raw_value(&extra)),
|
||||
extra,
|
||||
&owner,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
Ok::<_, Response>(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,747 @@
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use base64::{
|
||||
prelude::{BASE64_STANDARD, BASE64_URL_SAFE},
|
||||
Engine,
|
||||
};
|
||||
use hmac::{Hmac, Mac};
|
||||
use http::{header, HeaderMap, HeaderValue, StatusCode};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sha1::Sha1;
|
||||
use sha2::{Sha256, Sha512};
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
pub type HmacSha256 = Hmac<Sha256>;
|
||||
pub type HmacSha512 = Hmac<Sha512>;
|
||||
pub type HmacSha1 = Hmac<Sha1>;
|
||||
|
||||
mod github {
|
||||
use super::*;
|
||||
pub struct Github;
|
||||
|
||||
impl WebhookHandler for Github {
|
||||
fn handle_challenge_request<'header>(
|
||||
&self,
|
||||
_: &'header HeaderMap,
|
||||
_: &SignatureConfigData,
|
||||
_: &str,
|
||||
) -> Result<Option<Response>, AuthenticationError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_hmac_authentication_data<'payload, 'header, 'prefix>(
|
||||
&self,
|
||||
headers: &'header HeaderMap,
|
||||
raw_payload: &'payload str,
|
||||
) -> Result<SignatureAuthenticationData<'payload, 'header, 'prefix>, AuthenticationError>
|
||||
{
|
||||
let github_secret_header = headers.try_get_webhook_header("X-Hub-Signature-256")?;
|
||||
|
||||
let authentication_data = SignatureAuthenticationData::new(
|
||||
Cow::Borrowed(raw_payload),
|
||||
github_secret_header,
|
||||
Some("sha256="),
|
||||
SignatureAuthenticationDetails::new(HmacAlgorithm::Sha256, Encoding::Hex),
|
||||
);
|
||||
|
||||
Ok(authentication_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod slack {
|
||||
use super::*;
|
||||
pub struct Slack;
|
||||
|
||||
impl WebhookHandler for Slack {
|
||||
fn handle_challenge_request<'header>(
|
||||
&self,
|
||||
_: &'header HeaderMap,
|
||||
_: &SignatureConfigData,
|
||||
_: &str,
|
||||
) -> Result<Option<Response>, AuthenticationError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_hmac_authentication_data<'payload, 'header, 'prefix>(
|
||||
&self,
|
||||
headers: &'header HeaderMap,
|
||||
raw_payload: &'payload str,
|
||||
) -> Result<SignatureAuthenticationData<'payload, 'header, 'prefix>, AuthenticationError>
|
||||
{
|
||||
let slack_secret_signature = headers.try_get_webhook_header("X-Slack-Signature")?;
|
||||
let slack_timestamp_header =
|
||||
headers.try_get_webhook_header("X-Slack-Request-Timestamp")?;
|
||||
let signed_payload = format!("v0:{}:{}", slack_timestamp_header, raw_payload);
|
||||
|
||||
Ok(SignatureAuthenticationData::new(
|
||||
Cow::Owned(signed_payload),
|
||||
slack_secret_signature,
|
||||
Some("v0="),
|
||||
SignatureAuthenticationDetails::new(HmacAlgorithm::Sha256, Encoding::Hex),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod stripe {
|
||||
use super::*;
|
||||
|
||||
pub struct Stripe;
|
||||
|
||||
impl WebhookHandler for Stripe {
|
||||
fn handle_challenge_request<'header>(
|
||||
&self,
|
||||
_: &'header HeaderMap,
|
||||
_: &SignatureConfigData,
|
||||
_: &str,
|
||||
) -> Result<Option<Response>, AuthenticationError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_hmac_authentication_data<'payload, 'header, 'prefix>(
|
||||
&self,
|
||||
headers: &'header HeaderMap,
|
||||
raw_payload: &'payload str,
|
||||
) -> Result<SignatureAuthenticationData<'payload, 'header, 'prefix>, AuthenticationError>
|
||||
{
|
||||
let stripe_signature_header = headers.try_get_webhook_header("STRIPE-SIGNATURE")?;
|
||||
|
||||
let stripe_signature = parse_signature(stripe_signature_header, (",", "="));
|
||||
|
||||
let timestamp = *stripe_signature
|
||||
.get("t")
|
||||
.ok_or(AuthenticationError::InvalidTimestamp)?;
|
||||
let v1 = *stripe_signature
|
||||
.get("v1")
|
||||
.ok_or(AuthenticationError::InvalidSignature)?;
|
||||
|
||||
let signed_payload = format!("{}.{}", timestamp, raw_payload);
|
||||
|
||||
Ok(SignatureAuthenticationData::new(
|
||||
Cow::Owned(signed_payload),
|
||||
v1,
|
||||
None,
|
||||
SignatureAuthenticationDetails::new(HmacAlgorithm::Sha256, Encoding::Hex),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod tiktok {
|
||||
use super::*;
|
||||
|
||||
pub struct TikTok;
|
||||
|
||||
impl WebhookHandler for TikTok {
|
||||
fn handle_challenge_request<'header>(
|
||||
&self,
|
||||
_: &'header HeaderMap,
|
||||
_: &SignatureConfigData,
|
||||
_: &str,
|
||||
) -> Result<Option<Response>, AuthenticationError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_hmac_authentication_data<'payload, 'header, 'prefix>(
|
||||
&self,
|
||||
headers: &'header HeaderMap,
|
||||
raw_payload: &'payload str,
|
||||
) -> Result<SignatureAuthenticationData<'payload, 'header, 'prefix>, AuthenticationError>
|
||||
{
|
||||
let tiktok_secret_signature = headers.try_get_webhook_header("TikTok-Signature")?;
|
||||
|
||||
let stripe_signature = parse_signature(tiktok_secret_signature, (",", "="));
|
||||
|
||||
let timestamp = *stripe_signature
|
||||
.get("t")
|
||||
.ok_or(AuthenticationError::InvalidTimestamp)?;
|
||||
let s = *stripe_signature
|
||||
.get("s")
|
||||
.ok_or(AuthenticationError::InvalidSignature)?;
|
||||
|
||||
let signed_payload = format!("{}.{}", timestamp, raw_payload);
|
||||
|
||||
Ok(SignatureAuthenticationData::new(
|
||||
Cow::Owned(signed_payload),
|
||||
s,
|
||||
None,
|
||||
SignatureAuthenticationDetails::new(HmacAlgorithm::Sha256, Encoding::Hex),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod twitch {
|
||||
use super::*;
|
||||
use http::header;
|
||||
use serde_json::value::RawValue;
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TwitchCrcBody {
|
||||
challenge: String,
|
||||
#[allow(unused)]
|
||||
subscription: Box<RawValue>,
|
||||
}
|
||||
|
||||
pub struct Twitch;
|
||||
|
||||
impl WebhookHandler for Twitch {
|
||||
fn get_hmac_authentication_data<'payload, 'header, 'prefix>(
|
||||
&self,
|
||||
headers: &'header HeaderMap,
|
||||
raw_payload: &'payload str,
|
||||
) -> Result<SignatureAuthenticationData<'payload, 'header, 'prefix>, AuthenticationError>
|
||||
{
|
||||
let twitch_secret_signature =
|
||||
headers.try_get_webhook_header("Twitch-Eventsub-Message-Signature")?;
|
||||
let twitch_message_id_header =
|
||||
headers.try_get_webhook_header("Twitch-Eventsub-Message-Id")?;
|
||||
let twitch_timestamp_header =
|
||||
headers.try_get_webhook_header("Twitch-Eventsub-Message-Timestamp")?;
|
||||
|
||||
let message = format!(
|
||||
"{}{}{}",
|
||||
twitch_message_id_header, twitch_timestamp_header, raw_payload
|
||||
);
|
||||
|
||||
Ok(SignatureAuthenticationData::new(
|
||||
Cow::Owned(message),
|
||||
twitch_secret_signature,
|
||||
Some("sha256="),
|
||||
SignatureAuthenticationDetails::new(HmacAlgorithm::Sha256, Encoding::Hex),
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_challenge_request<'header>(
|
||||
&self,
|
||||
headers: &'header HeaderMap,
|
||||
signature_config_data: &SignatureConfigData,
|
||||
raw_payload: &str,
|
||||
) -> Result<Option<Response>, AuthenticationError> {
|
||||
let authentication_data = self.get_hmac_authentication_data(headers, raw_payload)?;
|
||||
verify_hmac_signature(authentication_data, &signature_config_data.secret_key)?;
|
||||
|
||||
let twitch_eventsub_message_type =
|
||||
headers.try_get_webhook_header("Twitch-Eventsub-Message-Type")?;
|
||||
|
||||
if twitch_eventsub_message_type != "webhook_callback_verification" {
|
||||
return Ok(None);
|
||||
}
|
||||
let twitch_crc_body =
|
||||
serde_json::from_str::<TwitchCrcBody>(raw_payload).map_err(|e| {
|
||||
AuthenticationError::InvalidChallengeResponse(format!(
|
||||
"Twitch :{}",
|
||||
e.to_string()
|
||||
))
|
||||
})?;
|
||||
|
||||
let response = (
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "text/plain")],
|
||||
twitch_crc_body.challenge.to_string(),
|
||||
);
|
||||
|
||||
Ok(Some(response.into_response()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod zoom {
|
||||
use axum::Json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct ZoomPayload {
|
||||
plain_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(unused)]
|
||||
struct ZoomChallengeResponse {
|
||||
payload: ZoomPayload,
|
||||
event_ts: u64,
|
||||
event: String,
|
||||
}
|
||||
|
||||
pub struct Zoom;
|
||||
|
||||
impl WebhookHandler for Zoom {
|
||||
fn handle_challenge_request<'header>(
|
||||
&self,
|
||||
_: &'header HeaderMap,
|
||||
signature_config_data: &SignatureConfigData,
|
||||
raw_payload: &str,
|
||||
) -> Result<Option<Response>, AuthenticationError> {
|
||||
let Ok(zoom_request_body) = serde_json::from_str::<ZoomChallengeResponse>(raw_payload)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if zoom_request_body.event != "endpoint.url_validation" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let hmac_signature = calculate_hmac_signature(
|
||||
HmacAlgorithm::Sha256,
|
||||
&signature_config_data.secret_key,
|
||||
&zoom_request_body.payload.plain_token,
|
||||
);
|
||||
|
||||
let encoded_hmac_signature = encode_hmac_signature(Encoding::Hex, &hmac_signature);
|
||||
|
||||
let response = (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"plainToken": zoom_request_body.payload.plain_token,
|
||||
"encryptedToken": encoded_hmac_signature
|
||||
})),
|
||||
);
|
||||
|
||||
Ok(Some(response.into_response()))
|
||||
}
|
||||
|
||||
fn get_hmac_authentication_data<'payload, 'header, 'prefix>(
|
||||
&self,
|
||||
headers: &'header HeaderMap,
|
||||
raw_payload: &'payload str,
|
||||
) -> Result<SignatureAuthenticationData<'payload, 'header, 'prefix>, AuthenticationError>
|
||||
{
|
||||
let zoom_signature_header = headers.try_get_webhook_header("x-zm-signature")?;
|
||||
let zoom_timestamp_header = headers.try_get_webhook_header("x-zm-request-timestamp")?;
|
||||
|
||||
let message = format!("v0:{}:{}", zoom_timestamp_header, raw_payload);
|
||||
|
||||
Ok(SignatureAuthenticationData::new(
|
||||
Cow::Owned(message),
|
||||
zoom_signature_header,
|
||||
Some("v0="),
|
||||
SignatureAuthenticationDetails::new(HmacAlgorithm::Sha256, Encoding::Hex),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use constant_time_eq::constant_time_eq;
|
||||
use github::Github;
|
||||
use slack::Slack;
|
||||
use stripe::Stripe;
|
||||
use tiktok::TikTok;
|
||||
use twitch::Twitch;
|
||||
use zoom::Zoom;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SignatureAuthenticationDetails {
|
||||
pub algorithm_to_use: HmacAlgorithm,
|
||||
pub header_key_encoding: Encoding,
|
||||
}
|
||||
|
||||
impl SignatureAuthenticationDetails {
|
||||
#[inline]
|
||||
fn new(algorithm_to_use: HmacAlgorithm, header_key_encoding: Encoding) -> Self {
|
||||
Self { algorithm_to_use, header_key_encoding }
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_signature<'header>(
|
||||
signature: &'header str,
|
||||
splitters: (&str, &str),
|
||||
) -> HashMap<&'header str, &'header str> {
|
||||
let headers: HashMap<&str, &str> = signature
|
||||
.split(splitters.0)
|
||||
.map(|header| {
|
||||
let mut key_and_value = header.split(splitters.1);
|
||||
let key = key_and_value.next();
|
||||
let value = key_and_value.next();
|
||||
(key, value)
|
||||
})
|
||||
.filter_map(|(key, value)| match (key, value) {
|
||||
(Some(key), Some(value)) => Some((key, value)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
headers
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SignatureAuthenticationData<'payload, 'header, 'prefix> {
|
||||
pub signed_payload: Cow<'payload, str>,
|
||||
pub header_key_value: &'header str,
|
||||
pub signature_prefix: Option<&'prefix str>,
|
||||
pub config: SignatureAuthenticationDetails,
|
||||
}
|
||||
|
||||
impl<'payload, 'header, 'prefix> SignatureAuthenticationData<'payload, 'header, 'prefix> {
|
||||
pub fn new(
|
||||
signed_payload: Cow<'payload, str>,
|
||||
header_key_value: &'header str,
|
||||
signature_prefix: Option<&'prefix str>,
|
||||
config: SignatureAuthenticationDetails,
|
||||
) -> Self {
|
||||
Self { signed_payload, header_key_value, signature_prefix, config }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WebhookHandler {
|
||||
fn handle_challenge_request<'header>(
|
||||
&self,
|
||||
headers: &'header HeaderMap,
|
||||
signature_config_data: &SignatureConfigData,
|
||||
raw_payload: &str,
|
||||
) -> Result<Option<Response>, AuthenticationError>;
|
||||
|
||||
fn get_hmac_authentication_data<'payload, 'header, 'prefix>(
|
||||
&self,
|
||||
headers: &'header HeaderMap,
|
||||
raw_payload: &'payload str,
|
||||
) -> Result<SignatureAuthenticationData<'payload, 'header, 'prefix>, AuthenticationError>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum HmacAlgorithm {
|
||||
Sha1,
|
||||
Sha256,
|
||||
Sha512,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Encoding {
|
||||
Base64,
|
||||
Base64Uri,
|
||||
Hex,
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SignatureAuthenticationMethod {
|
||||
algorithm: HmacAlgorithm,
|
||||
encoding: Encoding,
|
||||
signature_header_name: String,
|
||||
signature_prefix: Option<String>,
|
||||
}
|
||||
|
||||
pub struct SignatureConfigData<'config> {
|
||||
secret_key: &'config str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SignatureAuthentication {
|
||||
signature_provider: WebhookType,
|
||||
secret_key: String,
|
||||
authentication_config: Option<SignatureAuthenticationMethod>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BasicAuthAuthentication {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ApiKeyAuthentication {
|
||||
api_key_header: String,
|
||||
api_key_secret: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub enum WebhookType {
|
||||
Github,
|
||||
Slack,
|
||||
Stripe,
|
||||
TikTok,
|
||||
Twitch,
|
||||
Zoom,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl WebhookType {
|
||||
pub fn get_webhook_handler(&self) -> Option<&'static dyn WebhookHandler> {
|
||||
let handler: &'static dyn WebhookHandler = match *self {
|
||||
WebhookType::Github => &Github,
|
||||
WebhookType::Slack => &Slack,
|
||||
WebhookType::Stripe => &Stripe,
|
||||
WebhookType::TikTok => &TikTok,
|
||||
WebhookType::Twitch => &Twitch,
|
||||
WebhookType::Zoom => &Zoom,
|
||||
WebhookType::Custom => return None,
|
||||
};
|
||||
Some(handler)
|
||||
}
|
||||
}
|
||||
|
||||
trait TryGetWebhookHeader {
|
||||
fn try_get_webhook_header<'header>(
|
||||
&'header self,
|
||||
header_name: &str,
|
||||
) -> Result<&'header str, AuthenticationError>;
|
||||
}
|
||||
|
||||
impl TryGetWebhookHeader for HeaderMap<HeaderValue> {
|
||||
fn try_get_webhook_header<'header>(
|
||||
&'header self,
|
||||
header_name: &str,
|
||||
) -> Result<&'header str, AuthenticationError> {
|
||||
let Some(signature_header) = self.get(header_name) else {
|
||||
return Err(AuthenticationError::MissingHeader(header_name.to_string()));
|
||||
};
|
||||
let Some(signature_header) = signature_header.to_str().ok() else {
|
||||
return Err(AuthenticationError::InvalidHeader(header_name.to_string()));
|
||||
};
|
||||
|
||||
Ok(signature_header)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_hmac_signature(algorithm: HmacAlgorithm, secret: &str, payload: &str) -> Vec<u8> {
|
||||
match algorithm {
|
||||
HmacAlgorithm::Sha1 => {
|
||||
let mut mac =
|
||||
HmacSha1::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
|
||||
mac.update(payload.as_bytes());
|
||||
mac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
HmacAlgorithm::Sha256 => {
|
||||
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
|
||||
.expect("HMAC can take key of any size");
|
||||
mac.update(payload.as_bytes());
|
||||
mac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
HmacAlgorithm::Sha512 => {
|
||||
let mut mac = HmacSha512::new_from_slice(secret.as_bytes())
|
||||
.expect("HMAC can take key of any size");
|
||||
mac.update(payload.as_bytes());
|
||||
mac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_hmac_signature(encoding: Encoding, hmac_signature: &[u8]) -> String {
|
||||
match encoding {
|
||||
Encoding::Hex => hex::encode(hmac_signature),
|
||||
Encoding::Base64 => BASE64_STANDARD.encode(hmac_signature),
|
||||
Encoding::Base64Uri => BASE64_URL_SAFE.encode(hmac_signature),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_hmac_signature(
|
||||
authentication_data: SignatureAuthenticationData,
|
||||
webhook_signing_secret: &str,
|
||||
) -> Result<(), AuthenticationError> {
|
||||
let hmac_signature = calculate_hmac_signature(
|
||||
authentication_data.config.algorithm_to_use,
|
||||
&webhook_signing_secret,
|
||||
&authentication_data.signed_payload,
|
||||
);
|
||||
|
||||
let encoded_signature = encode_hmac_signature(
|
||||
authentication_data.config.header_key_encoding,
|
||||
&hmac_signature,
|
||||
);
|
||||
|
||||
let final_expected_signature =
|
||||
if let Some(signature_prefix) = authentication_data.signature_prefix {
|
||||
format!("{}{}", signature_prefix, encoded_signature)
|
||||
} else {
|
||||
encoded_signature
|
||||
};
|
||||
|
||||
if !constant_time_eq(
|
||||
final_expected_signature.as_bytes(),
|
||||
authentication_data.header_key_value.as_bytes(),
|
||||
) {
|
||||
return Err(AuthenticationError::InvalidSignature);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum AuthenticationMethod {
|
||||
Signature(SignatureAuthentication),
|
||||
BasicAuth(BasicAuthAuthentication),
|
||||
ApiKey(ApiKeyAuthentication),
|
||||
}
|
||||
|
||||
impl AuthenticationMethod {
|
||||
pub fn authenticate_http_request(
|
||||
&self,
|
||||
headers: &HeaderMap,
|
||||
raw_payload: Option<&String>,
|
||||
) -> Result<Option<Response>, AuthenticationError> {
|
||||
match self {
|
||||
AuthenticationMethod::Signature(SignatureAuthentication {
|
||||
secret_key,
|
||||
authentication_config,
|
||||
signature_provider,
|
||||
}) => {
|
||||
let raw_payload = raw_payload.ok_or(AuthenticationError::InvalidPayload)?;
|
||||
let config_data = SignatureConfigData { secret_key: &secret_key };
|
||||
let handler = signature_provider.get_webhook_handler();
|
||||
let challenge_response = handler
|
||||
.map(|handler| {
|
||||
handler.handle_challenge_request(headers, &config_data, raw_payload)
|
||||
})
|
||||
.transpose()?
|
||||
.flatten();
|
||||
|
||||
if let Some(challenge_response) = challenge_response {
|
||||
return Ok(Some(challenge_response));
|
||||
}
|
||||
|
||||
let authentication_data = match handler {
|
||||
Some(handler) => handler.get_hmac_authentication_data(headers, raw_payload)?,
|
||||
None => {
|
||||
let authentication_config = authentication_config
|
||||
.as_ref()
|
||||
.ok_or(AuthenticationError::InvalidCustomConfig)?;
|
||||
let signature_header_value = headers
|
||||
.try_get_webhook_header(&authentication_config.signature_header_name)?;
|
||||
SignatureAuthenticationData::new(
|
||||
Cow::Borrowed(raw_payload),
|
||||
signature_header_value,
|
||||
authentication_config.signature_prefix.as_deref(),
|
||||
SignatureAuthenticationDetails::new(
|
||||
authentication_config.algorithm,
|
||||
authentication_config.encoding,
|
||||
),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
verify_hmac_signature(authentication_data, &secret_key)?;
|
||||
}
|
||||
AuthenticationMethod::ApiKey(ApiKeyAuthentication {
|
||||
api_key_header,
|
||||
api_key_secret,
|
||||
}) => {
|
||||
let api_key_to_cmp = headers.try_get_webhook_header(&api_key_header)?;
|
||||
if api_key_to_cmp != api_key_secret {
|
||||
return Err(AuthenticationError::InvalidApiKey);
|
||||
}
|
||||
}
|
||||
AuthenticationMethod::BasicAuth(BasicAuthAuthentication { username, password }) => {
|
||||
let mut credentials_store = headers
|
||||
.try_get_webhook_header("Authorization")
|
||||
.map_err(|_| AuthenticationError::UnauthorizedBasicHttpAuth)?
|
||||
.split(' ');
|
||||
|
||||
let _ = credentials_store
|
||||
.next()
|
||||
.filter(|r#type| *r#type == "Basic")
|
||||
.ok_or(AuthenticationError::UnauthorizedBasicHttpAuth)?;
|
||||
|
||||
let credentials_as_base64 = credentials_store
|
||||
.next()
|
||||
.ok_or(AuthenticationError::UnauthorizedBasicHttpAuth)?;
|
||||
|
||||
let credentials_from_base64_as_bytes = BASE64_STANDARD
|
||||
.decode(credentials_as_base64.as_bytes())
|
||||
.map_err(|_| AuthenticationError::UnauthorizedBasicHttpAuth)?;
|
||||
|
||||
let credentials_separated_with_colon =
|
||||
String::from_utf8(credentials_from_base64_as_bytes)
|
||||
.map_err(|_| AuthenticationError::UnauthorizedBasicHttpAuth)?;
|
||||
|
||||
let credentials = credentials_separated_with_colon.split(':').collect_vec();
|
||||
|
||||
if credentials.len() != 2 {
|
||||
return Err(AuthenticationError::UnauthorizedBasicHttpAuth);
|
||||
}
|
||||
|
||||
if credentials.get(0).unwrap() != username
|
||||
|| credentials.get(1).unwrap() != password
|
||||
{
|
||||
return Err(AuthenticationError::UnauthorizedBasicHttpAuth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[allow(unused)]
|
||||
pub enum AuthenticationError {
|
||||
#[error("failed to parse timestamp")]
|
||||
InvalidTimestamp,
|
||||
|
||||
#[error("invalid secret")]
|
||||
InvalidSecret(#[from] base64::DecodeError),
|
||||
|
||||
#[error("invalid header `{0}`")]
|
||||
InvalidHeader(String),
|
||||
|
||||
#[error("signature timestamp too old")]
|
||||
TimestampTooOldError,
|
||||
|
||||
#[error("signature timestamp too far in future")]
|
||||
FutureTimestampError,
|
||||
|
||||
#[error("missing header {0}")]
|
||||
MissingHeader(String),
|
||||
|
||||
#[error("signature invalid")]
|
||||
InvalidSignature,
|
||||
|
||||
#[error("payload invalid")]
|
||||
InvalidPayload,
|
||||
|
||||
#[error("invalid custom config")]
|
||||
InvalidCustomConfig,
|
||||
|
||||
#[error("invalid auth header: {0}")]
|
||||
InvalidAuthHeader(String),
|
||||
|
||||
#[error("invalid api key")]
|
||||
InvalidApiKey,
|
||||
|
||||
#[error("invalid challenge response: {0}")]
|
||||
InvalidChallengeResponse(String),
|
||||
|
||||
#[error("")]
|
||||
UnauthorizedBasicHttpAuth,
|
||||
}
|
||||
|
||||
impl IntoResponse for AuthenticationError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error_message) = match &self {
|
||||
AuthenticationError::InvalidTimestamp
|
||||
| AuthenticationError::InvalidPayload
|
||||
| AuthenticationError::InvalidHeader(_)
|
||||
| AuthenticationError::MissingHeader(_)
|
||||
| AuthenticationError::TimestampTooOldError
|
||||
| AuthenticationError::FutureTimestampError
|
||||
| AuthenticationError::InvalidCustomConfig
|
||||
| AuthenticationError::InvalidChallengeResponse(_) => {
|
||||
(StatusCode::BAD_REQUEST, self.to_string())
|
||||
}
|
||||
|
||||
AuthenticationError::InvalidSecret(_)
|
||||
| AuthenticationError::InvalidSignature
|
||||
| AuthenticationError::InvalidAuthHeader(_) => {
|
||||
(StatusCode::UNAUTHORIZED, self.to_string())
|
||||
}
|
||||
AuthenticationError::UnauthorizedBasicHttpAuth => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
[(header::WWW_AUTHENTICATE, r#"Basic realm="Restricted Area""#)],
|
||||
"Unauthorized",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
AuthenticationError::InvalidApiKey => (StatusCode::FORBIDDEN, self.to_string()),
|
||||
};
|
||||
|
||||
let body = json!({ "error": error_message });
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
|
||||
|
||||
(status, headers, body.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::http_trigger_auth::{self};
|
||||
#[cfg(feature = "parquet")]
|
||||
use crate::job_helpers_ee::get_workspace_s3_resource;
|
||||
use crate::resources::try_get_resource_from_db_as;
|
||||
use crate::utils::non_empty_str;
|
||||
use crate::{
|
||||
args::try_from_request_body,
|
||||
auth::{AuthCache, OptTokened},
|
||||
@@ -10,6 +13,7 @@ use crate::{
|
||||
},
|
||||
users::fetch_api_authed,
|
||||
};
|
||||
use axum::response::Response;
|
||||
use axum::{
|
||||
extract::{Path, Query, Request},
|
||||
response::IntoResponse,
|
||||
@@ -26,6 +30,7 @@ use std::borrow::Cow;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tower_http::cors::CorsLayer;
|
||||
use windmill_audit::{audit_ee::audit_log, ActionKind};
|
||||
use windmill_common::error::Error;
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::s3_helpers::build_object_store_client;
|
||||
use windmill_common::{
|
||||
@@ -101,6 +106,18 @@ impl TryFrom<&http::Method> for HttpMethod {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
#[sqlx(type_name = "AUTHENTICATION_METHOD", rename_all = "snake_case")]
|
||||
#[serde(rename_all(serialize = "snake_case", deserialize = "snake_case"))]
|
||||
pub enum AuthenticationMethod {
|
||||
None,
|
||||
Windmill,
|
||||
ApiKey,
|
||||
BasicHttp,
|
||||
CustomScript,
|
||||
Signature,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NewTrigger {
|
||||
path: String,
|
||||
@@ -108,9 +125,10 @@ struct NewTrigger {
|
||||
script_path: String,
|
||||
is_flow: bool,
|
||||
is_async: bool,
|
||||
requires_auth: bool,
|
||||
http_method: HttpMethod,
|
||||
authentication_resource_path: Option<String>,
|
||||
authentication_method: AuthenticationMethod,
|
||||
static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
http_method: HttpMethod,
|
||||
workspaced_route: Option<bool>,
|
||||
is_static_website: bool,
|
||||
wrap_body: Option<bool>,
|
||||
@@ -130,11 +148,12 @@ pub struct HttpTrigger {
|
||||
pub edited_at: chrono::DateTime<chrono::Utc>,
|
||||
pub extra_perms: serde_json::Value,
|
||||
pub is_async: bool,
|
||||
pub requires_auth: bool,
|
||||
pub authentication_method: AuthenticationMethod,
|
||||
pub http_method: HttpMethod,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
pub is_static_website: bool,
|
||||
pub authentication_resource_path: Option<String>,
|
||||
pub workspaced_route: bool,
|
||||
pub wrap_body: bool,
|
||||
pub raw_string: bool,
|
||||
@@ -147,7 +166,9 @@ struct EditTrigger {
|
||||
script_path: String,
|
||||
is_flow: bool,
|
||||
is_async: bool,
|
||||
requires_auth: bool,
|
||||
authentication_method: AuthenticationMethod,
|
||||
#[serde(deserialize_with = "non_empty_str")]
|
||||
authentication_resource_path: Option<String>,
|
||||
http_method: HttpMethod,
|
||||
static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
workspaced_route: Option<bool>,
|
||||
@@ -190,9 +211,10 @@ async fn list_triggers(
|
||||
"edited_at",
|
||||
"extra_perms",
|
||||
"is_async",
|
||||
"requires_auth",
|
||||
"authentication_method",
|
||||
"static_asset_config",
|
||||
"is_static_website",
|
||||
"authentication_resource_path",
|
||||
])
|
||||
.order_by("edited_at", true)
|
||||
.and_where("workspace_id = ?".bind(&w_id))
|
||||
@@ -243,9 +265,10 @@ async fn get_trigger(
|
||||
edited_at,
|
||||
extra_perms,
|
||||
is_async,
|
||||
requires_auth,
|
||||
authentication_method as "authentication_method: _",
|
||||
static_asset_config as "static_asset_config: _",
|
||||
is_static_website,
|
||||
authentication_resource_path,
|
||||
wrap_body,
|
||||
raw_string
|
||||
FROM
|
||||
@@ -266,6 +289,23 @@ async fn get_trigger(
|
||||
Ok(Json(trigger))
|
||||
}
|
||||
|
||||
fn validate_authentication_method(
|
||||
authentication_method: AuthenticationMethod,
|
||||
raw_string: Option<bool>,
|
||||
) -> error::Result<()> {
|
||||
match (authentication_method, raw_string) {
|
||||
(AuthenticationMethod::CustomScript, raw) if !raw.unwrap_or(false) == true => {
|
||||
return Err(Error::BadRequest(
|
||||
"To use custom script authentication, please enable the raw body option."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -279,6 +319,8 @@ async fn create_trigger(
|
||||
return Err(error::Error::BadRequest("Invalid route path".to_string()));
|
||||
}
|
||||
|
||||
validate_authentication_method(ct.authentication_method, ct.raw_string)?;
|
||||
|
||||
// route path key is extracted from the route path to check for uniqueness
|
||||
// it replaces /?:{key} with :key
|
||||
// it will also remove the leading / if present, not an issue as we only allow : after slashes
|
||||
@@ -313,12 +355,13 @@ async fn create_trigger(
|
||||
route_path,
|
||||
route_path_key,
|
||||
workspaced_route,
|
||||
authentication_resource_path,
|
||||
wrap_body,
|
||||
raw_string,
|
||||
script_path,
|
||||
is_flow,
|
||||
is_async,
|
||||
requires_auth,
|
||||
authentication_method,
|
||||
http_method,
|
||||
static_asset_config,
|
||||
edited_by,
|
||||
@@ -327,20 +370,21 @@ async fn create_trigger(
|
||||
is_static_website
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, now(), $16
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), $17
|
||||
)
|
||||
"#,
|
||||
w_id,
|
||||
ct.path,
|
||||
ct.route_path,
|
||||
&route_path_key,
|
||||
ct.workspaced_route.unwrap_or(false),
|
||||
ct.workspaced_route,
|
||||
ct.authentication_resource_path,
|
||||
ct.wrap_body.unwrap_or(false),
|
||||
ct.raw_string.unwrap_or(false),
|
||||
ct.script_path,
|
||||
ct.is_flow,
|
||||
ct.is_async,
|
||||
ct.requires_auth,
|
||||
ct.authentication_method as _,
|
||||
ct.http_method as _,
|
||||
ct.static_asset_config as _,
|
||||
&authed.username,
|
||||
@@ -380,6 +424,8 @@ async fn update_trigger(
|
||||
));
|
||||
}
|
||||
|
||||
validate_authentication_method(ct.authentication_method, ct.raw_string)?;
|
||||
|
||||
let mut tx;
|
||||
if authed.is_admin {
|
||||
let Some(route_path) = ct.route_path else {
|
||||
@@ -417,29 +463,31 @@ async fn update_trigger(
|
||||
SET
|
||||
route_path = $1,
|
||||
route_path_key = $2,
|
||||
workspaced_route = $3,
|
||||
workspaced_route = $3,
|
||||
wrap_body = $4,
|
||||
raw_string = $5,
|
||||
script_path = $6,
|
||||
path = $7,
|
||||
is_flow = $8,
|
||||
http_method = $9,
|
||||
static_asset_config = $10,
|
||||
edited_by = $11,
|
||||
email = $12,
|
||||
is_async = $13,
|
||||
requires_auth = $14,
|
||||
authentication_resource_path = $6,
|
||||
script_path = $7,
|
||||
path = $8,
|
||||
is_flow = $9,
|
||||
http_method = $10,
|
||||
static_asset_config = $11,
|
||||
edited_by = $12,
|
||||
email = $13,
|
||||
is_async = $14,
|
||||
authentication_method = $15,
|
||||
edited_at = now(),
|
||||
is_static_website = $15
|
||||
is_static_website = $16
|
||||
WHERE
|
||||
workspace_id = $16 AND
|
||||
path = $17
|
||||
workspace_id = $17 AND
|
||||
path = $18
|
||||
"#,
|
||||
route_path,
|
||||
&route_path_key,
|
||||
ct.workspaced_route,
|
||||
ct.wrap_body,
|
||||
ct.raw_string,
|
||||
ct.authentication_resource_path,
|
||||
ct.script_path,
|
||||
ct.path,
|
||||
ct.is_flow,
|
||||
@@ -448,7 +496,7 @@ async fn update_trigger(
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
ct.is_async,
|
||||
ct.requires_auth,
|
||||
ct.authentication_method as _,
|
||||
ct.is_static_website,
|
||||
w_id,
|
||||
path,
|
||||
@@ -462,21 +510,29 @@ async fn update_trigger(
|
||||
UPDATE
|
||||
http_trigger
|
||||
SET
|
||||
script_path = $1,
|
||||
path = $2,
|
||||
is_flow = $3,
|
||||
http_method = $4,
|
||||
static_asset_config = $5,
|
||||
edited_by = $6,
|
||||
email = $7,
|
||||
is_async = $8,
|
||||
requires_auth = $9,
|
||||
workspaced_route = $1,
|
||||
wrap_body = $2,
|
||||
raw_string = $3,
|
||||
authentication_resource_path = $4,
|
||||
script_path = $5,
|
||||
path = $6,
|
||||
is_flow = $7,
|
||||
http_method = $8,
|
||||
static_asset_config = $9,
|
||||
edited_by = $10,
|
||||
email = $11,
|
||||
is_async = $12,
|
||||
authentication_method = $13,
|
||||
edited_at = now(),
|
||||
is_static_website = $10
|
||||
is_static_website = $14
|
||||
WHERE
|
||||
workspace_id = $11 AND
|
||||
path = $12
|
||||
workspace_id = $15 AND
|
||||
path = $16
|
||||
"#,
|
||||
ct.workspaced_route,
|
||||
ct.wrap_body,
|
||||
ct.raw_string,
|
||||
ct.authentication_resource_path,
|
||||
ct.script_path,
|
||||
ct.path,
|
||||
ct.is_flow,
|
||||
@@ -485,7 +541,7 @@ async fn update_trigger(
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
ct.is_async,
|
||||
ct.requires_auth,
|
||||
ct.authentication_method as _,
|
||||
ct.is_static_website,
|
||||
w_id,
|
||||
path,
|
||||
@@ -652,6 +708,7 @@ async fn exists_route(
|
||||
Ok(Json(exists))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TriggerRoute {
|
||||
path: String,
|
||||
script_path: String,
|
||||
@@ -659,11 +716,12 @@ struct TriggerRoute {
|
||||
route_path: String,
|
||||
workspace_id: String,
|
||||
is_async: bool,
|
||||
requires_auth: bool,
|
||||
authentication_method: AuthenticationMethod,
|
||||
edited_by: String,
|
||||
email: String,
|
||||
static_asset_config: Option<sqlx::types::Json<S3Object>>,
|
||||
is_static_website: bool,
|
||||
authentication_resource_path: Option<String>,
|
||||
workspaced_route: bool,
|
||||
wrap_body: bool,
|
||||
raw_string: bool,
|
||||
@@ -694,14 +752,15 @@ async fn get_http_route_trigger(
|
||||
route_path,
|
||||
workspace_id,
|
||||
is_async,
|
||||
requires_auth,
|
||||
authentication_method AS "authentication_method: _",
|
||||
edited_by,
|
||||
email,
|
||||
static_asset_config AS "static_asset_config: _",
|
||||
wrap_body,
|
||||
raw_string,
|
||||
workspaced_route,
|
||||
is_static_website
|
||||
is_static_website,
|
||||
authentication_resource_path
|
||||
FROM
|
||||
http_trigger
|
||||
WHERE
|
||||
@@ -723,9 +782,10 @@ async fn get_http_route_trigger(
|
||||
script_path,
|
||||
is_flow,
|
||||
route_path,
|
||||
authentication_resource_path,
|
||||
workspace_id,
|
||||
is_async,
|
||||
requires_auth,
|
||||
authentication_method AS "authentication_method: _",
|
||||
edited_by,
|
||||
email,
|
||||
static_asset_config AS "static_asset_config: _",
|
||||
@@ -787,7 +847,7 @@ async fn get_http_route_trigger(
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
|
||||
let username_override = if trigger.requires_auth {
|
||||
let username_override = if let AuthenticationMethod::Windmill = trigger.authentication_method {
|
||||
let opt_authed = if let Some(token) = token {
|
||||
auth_cache
|
||||
.get_authed(Some(trigger.workspace_id.clone()), token)
|
||||
@@ -878,9 +938,9 @@ async fn route_job(
|
||||
method: http::Method,
|
||||
headers: HeaderMap,
|
||||
request: Request,
|
||||
) -> impl IntoResponse {
|
||||
) -> Result<impl IntoResponse, Response> {
|
||||
let route_path = route_path.to_path().trim_end_matches("/");
|
||||
let (trigger, called_path, params, authed) = match get_http_route_trigger(
|
||||
let (trigger, called_path, params, authed) = get_http_route_trigger(
|
||||
route_path,
|
||||
&auth_cache,
|
||||
token.as_ref(),
|
||||
@@ -889,36 +949,82 @@ async fn route_job(
|
||||
&method,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(trigger) => trigger,
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
let result = try_from_request_body(
|
||||
let args = try_from_request_body(
|
||||
request,
|
||||
&db,
|
||||
Some(trigger.raw_string),
|
||||
Some(match trigger.authentication_method {
|
||||
AuthenticationMethod::CustomScript | AuthenticationMethod::Signature => true,
|
||||
_ => trigger.raw_string,
|
||||
}),
|
||||
Some(trigger.wrap_body),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
let mut args = match result {
|
||||
Ok(args) => match args
|
||||
.to_push_args_owned(&authed, &db, &trigger.workspace_id)
|
||||
.await
|
||||
{
|
||||
Ok(args) => args,
|
||||
Err(e) => return e.into_response(),
|
||||
},
|
||||
Err(e) => return e.into_response(),
|
||||
};
|
||||
let mut args = args
|
||||
.to_push_args_owned(&authed, &db, &trigger.workspace_id)
|
||||
.await
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
match trigger.authentication_method {
|
||||
AuthenticationMethod::None
|
||||
| AuthenticationMethod::Windmill
|
||||
| AuthenticationMethod::CustomScript => {}
|
||||
_ => {
|
||||
let resource_path = match trigger.authentication_resource_path {
|
||||
Some(resource_path) => resource_path,
|
||||
None => {
|
||||
return Err(Error::BadRequest(
|
||||
"Missing authentication resource path".to_string(),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
};
|
||||
|
||||
let authentication_method =
|
||||
try_get_resource_from_db_as::<http_trigger_auth::AuthenticationMethod>(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
&db,
|
||||
&resource_path,
|
||||
&trigger.workspace_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
let raw_payload = args
|
||||
.extra
|
||||
.as_ref()
|
||||
.and_then(|extra| {
|
||||
extra
|
||||
.get("raw_string")
|
||||
.and_then(|value| Some(value.to_string()))
|
||||
.and_then(|raw_payload| Some(serde_json::from_str::<String>(&raw_payload)))
|
||||
})
|
||||
.transpose()
|
||||
.map_err(|e| {
|
||||
windmill_common::error::Error::SerdeJson { location: e.to_string(), error: e }
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
let response = authentication_method
|
||||
.authenticate_http_request(&headers, raw_payload.as_ref())
|
||||
.map_err(|e| e.into_response())?;
|
||||
|
||||
if let Some(response) = response {
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "parquet"))]
|
||||
if trigger.static_asset_config.is_some() {
|
||||
return error::Error::internal_err(
|
||||
return Err(error::Error::internal_err(
|
||||
"Static asset configuration is not supported in this build".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
.into_response());
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
@@ -1018,9 +1124,9 @@ async fn route_job(
|
||||
};
|
||||
match build_static_response_f.await {
|
||||
Ok((status, headers, body_stream)) => {
|
||||
return (status, headers, body_stream).into_response()
|
||||
return Ok((status, headers, body_stream).into_response())
|
||||
}
|
||||
Err(e) => return e.into_response(),
|
||||
Err(e) => return Err(e.into_response()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1041,7 +1147,7 @@ async fn route_job(
|
||||
|
||||
let run_query = RunJobQuery::default();
|
||||
|
||||
if trigger.is_flow {
|
||||
let response = if trigger.is_flow {
|
||||
if trigger.is_async {
|
||||
run_flow_by_path_inner(
|
||||
authed,
|
||||
@@ -1097,5 +1203,7 @@ async fn route_job(
|
||||
.await
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@ mod granular_acls;
|
||||
mod groups;
|
||||
#[cfg(feature = "http_trigger")]
|
||||
mod http_triggers;
|
||||
#[cfg(feature = "http_trigger")]
|
||||
mod http_trigger_auth;
|
||||
mod indexer_ee;
|
||||
mod inputs;
|
||||
mod integration;
|
||||
|
||||
@@ -1209,6 +1209,7 @@ async fn update_resource_type(
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
feature = "http_trigger",
|
||||
feature = "postgres_trigger",
|
||||
feature = "mqtt_trigger",
|
||||
all(feature = "sqs_trigger", feature = "enterprise")
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
use axum::{body::Body, response::Response};
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use sqlx::{Postgres, Transaction};
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::worker::CLOUD_HOSTED;
|
||||
@@ -190,6 +190,15 @@ pub fn content_plain(body: Body) -> Response {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn non_empty_str<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let o: Option<String> = Option::deserialize(deserializer)?;
|
||||
Ok(o.filter(|s| !s.trim().is_empty()))
|
||||
}
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
@@ -542,6 +542,7 @@ pub(crate) async fn tarball_workspace(
|
||||
path,
|
||||
route_path,
|
||||
route_path_key,
|
||||
authentication_resource_path,
|
||||
script_path,
|
||||
is_flow,
|
||||
edited_by,
|
||||
@@ -549,7 +550,7 @@ pub(crate) async fn tarball_workspace(
|
||||
email,
|
||||
extra_perms,
|
||||
is_async,
|
||||
requires_auth,
|
||||
authentication_method AS "authentication_method: _",
|
||||
http_method AS "http_method: _",
|
||||
static_asset_config AS "static_asset_config: _",
|
||||
is_static_website,
|
||||
|
||||
@@ -173,9 +173,6 @@ pub fn to_anyhow<T: 'static + std::error::Error + Send + Sync>(e: T) -> anyhow::
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let e = &self;
|
||||
let body = Body::from(e.to_string());
|
||||
|
||||
let status = match self {
|
||||
Self::NotFound(_) => axum::http::StatusCode::NOT_FOUND,
|
||||
Self::NotAuthorized(_) => axum::http::StatusCode::UNAUTHORIZED,
|
||||
@@ -187,12 +184,16 @@ impl IntoResponse for Error {
|
||||
_ => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
let e = &self;
|
||||
|
||||
if matches!(status, axum::http::StatusCode::NOT_FOUND) {
|
||||
tracing::warn!(message = e.to_string());
|
||||
} else {
|
||||
tracing::error!(message = e.to_string(), error = ?e);
|
||||
};
|
||||
|
||||
let body = Body::from(e.to_string());
|
||||
|
||||
axum::response::Response::builder()
|
||||
.header("Content-Type", "text/plain")
|
||||
.status(status)
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
)}
|
||||
style={wrapperStyle}
|
||||
>
|
||||
{#if href}
|
||||
{#if href && !disabled}
|
||||
<a
|
||||
bind:this={element}
|
||||
on:pointerdown
|
||||
|
||||
@@ -255,6 +255,9 @@
|
||||
runnableArgs={data?.args}
|
||||
bind:route_path={args.route_path}
|
||||
bind:http_method={args.http_method}
|
||||
bind:raw_string={args.raw_string}
|
||||
bind:wrap_body={args.wrap_body}
|
||||
capture_mode={true}
|
||||
headless
|
||||
{captureInfo}
|
||||
bind:captureTable
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
can_write={true}
|
||||
bind:route_path={args.route_path}
|
||||
bind:http_method={args.http_method}
|
||||
bind:raw_string={args.raw_string}
|
||||
bind:wrap_body={args.wrap_body}
|
||||
capture_mode={true}
|
||||
headless
|
||||
/>
|
||||
{:else if triggerType === 'email'}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
export let raw_string: boolean
|
||||
export let wrap_body: boolean
|
||||
</script>
|
||||
|
||||
<Label label="Raw body" class="w-full">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip>
|
||||
Provides the raw JSON payload as a string under the 'raw_string' key. Required for custom
|
||||
script authentication method and useful for signature verification or other advanced use
|
||||
cases.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
<Toggle
|
||||
checked={raw_string}
|
||||
on:change={() => {
|
||||
raw_string = !raw_string
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
<Label label="Wrap body" class="w-full">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip
|
||||
>Wraps the payload in an object under the 'body' key, useful for handling unknown payloads.</Tooltip
|
||||
>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
<Toggle
|
||||
checked={wrap_body}
|
||||
on:change={() => {
|
||||
wrap_body = !wrap_body
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
@@ -15,10 +15,11 @@
|
||||
import CaptureSection from '../CaptureSection.svelte'
|
||||
import CaptureTable from '../CaptureTable.svelte'
|
||||
import ClipboardPanel from '../../details/ClipboardPanel.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { isObject } from '$lib/utils'
|
||||
import { getHttpRoute } from './utils'
|
||||
import RouteBodyTransformerOption from './RouteBodyTransformerOption.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
export let initialTriggerPath: string | undefined = undefined
|
||||
export let dirtyRoutePath: boolean = false
|
||||
@@ -34,6 +35,9 @@
|
||||
export let workspaced_route: boolean = false
|
||||
export let isValid = false
|
||||
export let runnableArgs: any = {}
|
||||
export let raw_string: boolean = false
|
||||
export let wrap_body: boolean = false
|
||||
export let capture_mode: boolean
|
||||
let validateTimeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
let routeError: string = ''
|
||||
@@ -187,7 +191,7 @@
|
||||
<div class="text-red-600 dark:text-red-400 text-2xs mt-1.5"
|
||||
>{dirtyRoutePath ? routeError : ''}</div
|
||||
>
|
||||
{#if !isCloudHosted()}
|
||||
{#if !capture_mode && !isCloudHosted()}
|
||||
<div class="mt-1">
|
||||
<Toggle
|
||||
size="sm"
|
||||
@@ -205,6 +209,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if capture_mode}
|
||||
<RouteBodyTransformerOption bind:raw_string bind:wrap_body />
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import { HttpTriggerService } from '$lib/gen'
|
||||
import { HttpTriggerService, VariableService, type AuthenticationMethod } from '$lib/gen'
|
||||
import { usedTriggerKinds, userStore, workspaceStore } from '$lib/stores'
|
||||
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { Loader2, Save, Pipette } from 'lucide-svelte'
|
||||
import { Loader2, Save, Pipette, Plus } from 'lucide-svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import VariableEditor from '../../VariableEditor.svelte'
|
||||
import { json } from 'svelte-highlight/languages'
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import JsonEditor from '$lib/components/JsonEditor.svelte'
|
||||
@@ -23,7 +24,12 @@
|
||||
import RouteEditorConfigSection from './RouteEditorConfigSection.svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
|
||||
import ItemPicker from '../../ItemPicker.svelte'
|
||||
import { Popover } from '$lib/components/meltComponents'
|
||||
import { HUB_SCRIPT_ID, SECRET_KEY_PATH } from './utils'
|
||||
import { HubFlow } from '$lib/hub'
|
||||
import RouteBodyTransformerOption from './RouteBodyTransformerOption.svelte'
|
||||
let is_flow: boolean = false
|
||||
let initialPath = ''
|
||||
let edit = true
|
||||
@@ -40,13 +46,12 @@
|
||||
let isValid = false
|
||||
let dirtyRoutePath = false
|
||||
let is_async = false
|
||||
let requires_auth = false
|
||||
let authentication_method: AuthenticationMethod = 'none'
|
||||
let route_path = ''
|
||||
let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post'
|
||||
let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined =
|
||||
undefined
|
||||
let is_static_website = false
|
||||
|
||||
let is_static_website: boolean = false
|
||||
let s3FilePicker: S3FilePicker
|
||||
let s3FileUploadRawMode = false
|
||||
let s3Editor: SimpleEditor | undefined = undefined
|
||||
@@ -54,6 +59,55 @@
|
||||
let raw_string = false
|
||||
let wrap_body = false
|
||||
let drawerLoading = true
|
||||
let authentication_resource_path: string = ''
|
||||
let variablePicker: ItemPicker
|
||||
let variableEditor: VariableEditor
|
||||
let variable_path: string = ''
|
||||
type AuthenticationOption = {
|
||||
label: string
|
||||
value: AuthenticationMethod
|
||||
tooltip?: string
|
||||
resource_type?: string
|
||||
}
|
||||
|
||||
async function loadVariables() {
|
||||
return await VariableService.listVariable({ workspace: $workspaceStore ?? '' })
|
||||
}
|
||||
|
||||
let signature_options_type: 'custom_script' | 'custom_signature' = 'custom_signature'
|
||||
|
||||
const authentication_options: AuthenticationOption[] = [
|
||||
{
|
||||
label: 'No Auth',
|
||||
value: 'none'
|
||||
},
|
||||
{
|
||||
label: 'Windmill',
|
||||
value: 'windmill',
|
||||
tooltip: 'Requires the user to be authenticated with read access to this route'
|
||||
},
|
||||
{
|
||||
label: 'API Key',
|
||||
value: 'api_key',
|
||||
resource_type: 'api_key_auth',
|
||||
tooltip:
|
||||
'Checks for a valid API key in a specified header. Header name can be configured in the resource.'
|
||||
},
|
||||
{
|
||||
label: 'Basic Auth',
|
||||
value: 'basic_http',
|
||||
resource_type: 'basic_http_auth',
|
||||
tooltip:
|
||||
'Uses base64-encoded Basic authentication. Defaults to the Authorization header, but a custom header can be configured in the resource.'
|
||||
},
|
||||
{
|
||||
label: 'Signature',
|
||||
value: 'signature',
|
||||
tooltip:
|
||||
'Validates requests using HMAC or signature-based authentication. Can be custom or follow predefined formats from common providers.'
|
||||
}
|
||||
]
|
||||
|
||||
export async function openEdit(ePath: string, isFlow: boolean) {
|
||||
drawerLoading = true
|
||||
try {
|
||||
@@ -83,7 +137,7 @@
|
||||
edit = false
|
||||
itemKind = nis_flow ? 'flow' : 'script'
|
||||
is_async = false
|
||||
requires_auth = false
|
||||
authentication_method = 'none'
|
||||
route_path = defaultValues?.route_path ?? ''
|
||||
dirtyRoutePath = false
|
||||
http_method = defaultValues?.http_method ?? 'post'
|
||||
@@ -97,6 +151,11 @@
|
||||
dirtyPath = false
|
||||
is_static_website = false
|
||||
workspaced_route = false
|
||||
authentication_resource_path = ''
|
||||
variable_path = ''
|
||||
signature_options_type = 'custom_signature'
|
||||
raw_string = defaultValues?.raw_string ?? false
|
||||
wrap_body = defaultValues?.wrap_body ?? false
|
||||
} finally {
|
||||
drawerLoading = false
|
||||
}
|
||||
@@ -118,11 +177,17 @@
|
||||
route_path = s.route_path
|
||||
http_method = s.http_method ?? 'post'
|
||||
is_async = s.is_async
|
||||
requires_auth = s.requires_auth
|
||||
workspaced_route = s.workspaced_route
|
||||
wrap_body = s.wrap_body
|
||||
raw_string = s.raw_string
|
||||
|
||||
authentication_resource_path = s.authentication_resource_path ?? ''
|
||||
if (s.authentication_method === 'custom_script') {
|
||||
authentication_method = 'signature'
|
||||
signature_options_type = 'custom_script'
|
||||
} else {
|
||||
authentication_method = s.authentication_method
|
||||
signature_options_type = 'custom_signature'
|
||||
}
|
||||
if (!isCloudHosted()) {
|
||||
static_asset_config = s.static_asset_config
|
||||
s3FileUploadRawMode = !!static_asset_config
|
||||
@@ -133,6 +198,14 @@
|
||||
}
|
||||
|
||||
async function triggerScript(): Promise<void> {
|
||||
// If the user selects "signature" with the "custom_script" option,
|
||||
// we explicitly set the authentication method to "custom_script"
|
||||
// (which is a valid enum on its own in the backend)
|
||||
const auth_method: AuthenticationMethod =
|
||||
authentication_method === 'signature' && signature_options_type === 'custom_script'
|
||||
? 'custom_script'
|
||||
: authentication_method
|
||||
|
||||
if (edit) {
|
||||
await HttpTriggerService.updateHttpTrigger({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -142,12 +215,13 @@
|
||||
script_path,
|
||||
is_flow,
|
||||
is_async,
|
||||
requires_auth,
|
||||
authentication_method: auth_method,
|
||||
route_path: $userStore?.is_admin || $userStore?.is_super_admin ? route_path : undefined,
|
||||
http_method,
|
||||
static_asset_config,
|
||||
is_static_website,
|
||||
workspaced_route,
|
||||
authentication_resource_path,
|
||||
wrap_body,
|
||||
raw_string
|
||||
}
|
||||
@@ -161,12 +235,13 @@
|
||||
script_path,
|
||||
is_flow,
|
||||
is_async,
|
||||
requires_auth,
|
||||
authentication_method: auth_method,
|
||||
route_path,
|
||||
http_method,
|
||||
static_asset_config,
|
||||
is_static_website,
|
||||
workspaced_route,
|
||||
authentication_resource_path,
|
||||
wrap_body,
|
||||
raw_string
|
||||
}
|
||||
@@ -179,7 +254,6 @@
|
||||
dispatch('update')
|
||||
drawer.closeDrawer()
|
||||
}
|
||||
|
||||
let drawer: Drawer
|
||||
|
||||
let dirtyPath = false
|
||||
@@ -259,7 +333,7 @@
|
||||
is_async = false
|
||||
is_static_website = ev.detail === 'static_website'
|
||||
if (is_static_website) {
|
||||
requires_auth = false
|
||||
authentication_method = 'none'
|
||||
}
|
||||
} else if (ev.detail === 'runnable') {
|
||||
static_asset_config = undefined
|
||||
@@ -349,31 +423,35 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-xs mt-0.5 mb-1 text-tertiary">
|
||||
<p class="text-xs mt-3 mb-1 text-tertiary">
|
||||
Pick a script or flow to be triggered<Required required={true} /><br />
|
||||
To handle headers, query or path parameters, add a preprocessor to your runnable.
|
||||
</p>
|
||||
<div class="flex flex-row mb-2">
|
||||
<ScriptPicker
|
||||
disabled={fixedScriptPath != '' || !can_write}
|
||||
initialPath={fixedScriptPath || initialScriptPath}
|
||||
kinds={['script']}
|
||||
allowFlow={true}
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
allowRefresh={can_write}
|
||||
allowEdit={!$userStore?.operator}
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row mb-2">
|
||||
<ScriptPicker
|
||||
disabled={fixedScriptPath != '' || !can_write}
|
||||
initialPath={fixedScriptPath || initialScriptPath}
|
||||
kinds={['script']}
|
||||
allowFlow={true}
|
||||
bind:itemKind
|
||||
bind:scriptPath={script_path}
|
||||
allowRefresh={can_write}
|
||||
allowEdit={!$userStore?.operator}
|
||||
/>
|
||||
|
||||
{#if script_path === undefined}
|
||||
<Button
|
||||
btnClasses="ml-4 mt-2"
|
||||
color="dark"
|
||||
size="xs"
|
||||
href={itemKind === 'flow' ? '/flows/add?hub=62' : '/scripts/add?hub=hub%2F11627'}
|
||||
target="_blank">Create from template</Button
|
||||
>
|
||||
{/if}
|
||||
{#if script_path === undefined}
|
||||
<Button
|
||||
btnClasses="ml-4 mt-2"
|
||||
color="dark"
|
||||
size="xs"
|
||||
href={itemKind === 'flow'
|
||||
? '/flows/add?hub=62'
|
||||
: '/scripts/add?hub=hub%2F11627'}
|
||||
target="_blank">Create from template</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Section>
|
||||
@@ -386,6 +464,7 @@
|
||||
bind:http_method
|
||||
bind:workspaced_route
|
||||
{can_write}
|
||||
capture_mode={false}
|
||||
bind:static_asset_config
|
||||
/>
|
||||
|
||||
@@ -426,55 +505,124 @@
|
||||
<svelte:fragment slot="action">
|
||||
<ToggleButtonGroup
|
||||
class="w-auto h-full"
|
||||
selected={requires_auth ? 'required' : 'none'}
|
||||
on:selected={({ detail }) => {
|
||||
requires_auth = detail === 'required'
|
||||
bind:selected={authentication_method}
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'signature' && signature_options_type === 'custom_script') {
|
||||
raw_string = true
|
||||
}
|
||||
}}
|
||||
disabled={!can_write}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton label="None" value="none" {item} />
|
||||
<ToggleButton
|
||||
label="Required"
|
||||
value="required"
|
||||
tooltip="Requires authentication with read access on the route"
|
||||
{item}
|
||||
/>
|
||||
{#each authentication_options as option}
|
||||
{#if option.value === 'signature'}
|
||||
<Popover placement="top-end" usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger">
|
||||
<ToggleButton
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
tooltip={option.tooltip}
|
||||
{item}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<ToggleButtonGroup
|
||||
class="w-auto h-full"
|
||||
bind:selected={signature_options_type}
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'custom_script') {
|
||||
if (!raw_string) {
|
||||
raw_string = true
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={!can_write}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton
|
||||
label="Signature validation"
|
||||
value="custom_signature"
|
||||
tooltip="Use a predefined or custom signature-based authentication scheme"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
label="Custom script"
|
||||
value="custom_script"
|
||||
tooltip="Use your own script logic"
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{:else}
|
||||
<ToggleButton
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
tooltip={option.tooltip}
|
||||
{item}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
<Label label="Raw string" class="w-full">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip
|
||||
>Provides the raw JSON payload as a string under the 'raw_string' key, useful for
|
||||
signature verification and other use cases.</Tooltip
|
||||
>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
<Toggle
|
||||
checked={raw_string}
|
||||
on:change={() => {
|
||||
raw_string = !raw_string
|
||||
}}
|
||||
|
||||
{#each authentication_options as option}
|
||||
{#if option.resource_type && authentication_method === option.value}
|
||||
<ResourcePicker
|
||||
bind:value={authentication_resource_path}
|
||||
resourceType={option.resource_type}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
<Label label="Wrap body" class="w-full">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip
|
||||
>Wraps the payload in an object under the 'body' key, useful for handling
|
||||
unknown payloads.</Tooltip
|
||||
>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
<Toggle
|
||||
checked={wrap_body}
|
||||
on:change={() => {
|
||||
wrap_body = !wrap_body
|
||||
}}
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if authentication_method === 'signature'}
|
||||
{#if signature_options_type === 'custom_signature'}
|
||||
<ResourcePicker
|
||||
bind:value={authentication_resource_path}
|
||||
resourceType={'signature_auth'}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
{:else if signature_options_type === 'custom_script'}
|
||||
<p class="text-xs mt-3 mb-1 text-tertiary">
|
||||
Pick a secret variable or create one which will be used as a secret key for your
|
||||
custom script/flow<Required required={true} /><br />
|
||||
</p>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="flex flex-row gap-2 w-full">
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
bind:value={variable_path}
|
||||
readonly
|
||||
disabled={true}
|
||||
/>
|
||||
<Button
|
||||
title="Add variable"
|
||||
on:click={variablePicker.openDrawer}
|
||||
size="xs"
|
||||
color="dark"
|
||||
>
|
||||
Pick variable
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
disabled={emptyString(variable_path)}
|
||||
color="dark"
|
||||
size="xs"
|
||||
href={itemKind === 'flow'
|
||||
? `/flows/add?${SECRET_KEY_PATH}=${encodeURIComponent(variable_path)}&hub=${
|
||||
HubFlow.SIGNATURE_TEMPLATE
|
||||
}`
|
||||
: `/scripts/add?${SECRET_KEY_PATH}=${encodeURIComponent(
|
||||
variable_path
|
||||
)}&hub=hub%2F${HUB_SCRIPT_ID}`}
|
||||
target="_blank">Create from template</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<RouteBodyTransformerOption bind:raw_string bind:wrap_body />
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
@@ -482,3 +630,32 @@
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<ItemPicker
|
||||
bind:this={variablePicker}
|
||||
pickCallback={(path, _) => {
|
||||
variable_path = path
|
||||
}}
|
||||
tooltip="Variables are dynamic values that have a key associated to them and can be retrieved during the execution of a Script or Flow."
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets"
|
||||
itemName="Variable"
|
||||
extraField="path"
|
||||
loadItems={loadVariables}
|
||||
buttons={{ 'Edit/View': (x) => variableEditor.editVariable(x) }}
|
||||
>
|
||||
<div slot="submission" class="flex flex-row">
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
startIcon={{ icon: Plus }}
|
||||
on:click={() => {
|
||||
variableEditor.initNew()
|
||||
}}
|
||||
>
|
||||
New variable
|
||||
</Button>
|
||||
</div>
|
||||
</ItemPicker>
|
||||
|
||||
<VariableEditor bind:this={variableEditor} on:create={variablePicker.openDrawer} />
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import { base } from "$lib/base";
|
||||
import { isCloudHosted } from "$lib/cloud";
|
||||
import { base } from '$lib/base'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
|
||||
export function getHttpRoute(route_path: string | undefined, workspaced_route: boolean, workspace_id: string) {
|
||||
return `${location.origin}${base}/api/r/${
|
||||
isCloudHosted() || workspaced_route ? workspace_id + '/' : ''
|
||||
}${route_path ?? ''}`
|
||||
}
|
||||
export const SECRET_KEY_PATH = 'secret_key_path'
|
||||
export const HUB_SCRIPT_ID = 11663
|
||||
export const SIGNATURE_TEMPLATE_SCRIPT_HUB_PATH: string = `hub/${HUB_SCRIPT_ID}`
|
||||
export const SIGNATURE_TEMPLATE_FLOW_HUB_ID = '67'
|
||||
|
||||
export function getHttpRoute(
|
||||
route_path: string | undefined,
|
||||
workspaced_route: boolean,
|
||||
workspace_id: string
|
||||
) {
|
||||
return `${location.origin}${base}/api/r/${
|
||||
isCloudHosted() || workspaced_route ? workspace_id + '/' : ''
|
||||
}${route_path ?? ''}`
|
||||
}
|
||||
|
||||
export function replacePlaceholderForSignatureScriptTemplate(content: string) {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const secret_key_path = params.get(SECRET_KEY_PATH) ?? ''
|
||||
return content.replace(
|
||||
/(const\s+SECRET_KEY_VARIABLE_PATH\s*=\s*")[^"]*(";)/,
|
||||
`$1${secret_key_path}$2`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import type { Schema } from './common'
|
||||
import { AppService, FlowService, type Flow, type Script } from './gen'
|
||||
import { encodeState } from './utils'
|
||||
import rawHubPaths from './hubPaths.json?raw'
|
||||
import {
|
||||
replacePlaceholderForSignatureScriptTemplate,
|
||||
SIGNATURE_TEMPLATE_FLOW_HUB_ID,
|
||||
SIGNATURE_TEMPLATE_SCRIPT_HUB_PATH
|
||||
} from './components/triggers/http/utils'
|
||||
|
||||
export function scriptToHubUrl(
|
||||
content: string,
|
||||
@@ -19,6 +24,24 @@ export function scriptToHubUrl(
|
||||
return url
|
||||
}
|
||||
|
||||
export const HubScript = {
|
||||
SIGNATURE_TEMPLATE: SIGNATURE_TEMPLATE_SCRIPT_HUB_PATH
|
||||
} as const
|
||||
|
||||
export const HubFlow = {
|
||||
SIGNATURE_TEMPLATE: SIGNATURE_TEMPLATE_FLOW_HUB_ID
|
||||
} as const
|
||||
|
||||
export function replaceScriptPlaceholderWithItsValues(id: string, content: string) {
|
||||
switch (id) {
|
||||
case HubScript.SIGNATURE_TEMPLATE:
|
||||
case HubFlow.SIGNATURE_TEMPLATE:
|
||||
return replacePlaceholderForSignatureScriptTemplate(content)
|
||||
default:
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadHubFlows() {
|
||||
try {
|
||||
const flows = (await FlowService.listHubFlows()).flows ?? []
|
||||
|
||||
@@ -318,8 +318,9 @@ export async function getTriggerValue(kind: TriggerKind, path: string, workspace
|
||||
route_path,
|
||||
static_asset_config,
|
||||
is_async,
|
||||
requires_auth,
|
||||
is_static_website
|
||||
authentication_method,
|
||||
is_static_website,
|
||||
authentication_resource_path
|
||||
} = await HttpTriggerService.getHttpTrigger({
|
||||
workspace: workspace!,
|
||||
path: path
|
||||
@@ -332,8 +333,9 @@ export async function getTriggerValue(kind: TriggerKind, path: string, workspace
|
||||
route_path,
|
||||
static_asset_config,
|
||||
is_async,
|
||||
requires_auth,
|
||||
is_static_website
|
||||
authentication_method,
|
||||
is_static_website,
|
||||
authentication_resource_path
|
||||
}
|
||||
} else if (kind === 'schedules') {
|
||||
const {
|
||||
@@ -479,12 +481,12 @@ export async function getTriggerDependency(kind: TriggerKind, path: string, work
|
||||
}
|
||||
})
|
||||
} else if (kind === 'routes') {
|
||||
const { script_path, is_flow } = await HttpTriggerService.getHttpTrigger({
|
||||
const { script_path, is_flow, authentication_resource_path} = await HttpTriggerService.getHttpTrigger({
|
||||
workspace: workspace!,
|
||||
path: path
|
||||
})
|
||||
|
||||
result = retrieveKindsValues({ script_path, is_flow })
|
||||
result = retrieveKindsValues({ script_path, is_flow, resource_path: authentication_resource_path})
|
||||
} else if (kind === 'schedules') {
|
||||
const { script_path, is_flow } = await ScheduleService.getSchedule({
|
||||
workspace: workspace!,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import type { ScheduleTrigger } from '$lib/components/triggers'
|
||||
import type { GetInitialAndModifiedValues } from '$lib/components/common/confirmationModal/unsavedTypes'
|
||||
import { replaceScriptPlaceholderWithItsValues } from '$lib/hub'
|
||||
|
||||
let nodraft = $page.url.searchParams.get('nodraft')
|
||||
|
||||
@@ -130,6 +131,12 @@
|
||||
delete hub['comments']
|
||||
initialPath = `u/${$userStore?.username}/flow_${hubId}`
|
||||
Object.assign(flow, hub.flow)
|
||||
if (flow.value.preprocessor_module?.value.type === 'rawscript') {
|
||||
flow.value.preprocessor_module.value.content = replaceScriptPlaceholderWithItsValues(
|
||||
hubId,
|
||||
flow.value.preprocessor_module.value.content
|
||||
)
|
||||
}
|
||||
flow = flow
|
||||
goto('?', { replaceState: true })
|
||||
selectedId = 'constants'
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import type { ScheduleTrigger } from '$lib/components/triggers'
|
||||
import type { GetInitialAndModifiedValues } from '$lib/components/common/confirmationModal/unsavedTypes'
|
||||
import { replaceScriptPlaceholderWithItsValues } from '$lib/hub'
|
||||
|
||||
// Default
|
||||
let schema: Schema = emptySchema()
|
||||
@@ -87,7 +88,7 @@
|
||||
path: hubPath
|
||||
})
|
||||
script.description = `Fork of ${hubPath}`
|
||||
script.content = content
|
||||
script.content = replaceScriptPlaceholderWithItsValues(hubPath, content)
|
||||
script.summary = summary ?? ''
|
||||
script.language = language as Script['language']
|
||||
scriptBuilder?.setCode(script.content)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import { Code, Eye, Pen, Plus, Share, Trash, Circle, Database, FileUp } from 'lucide-svelte'
|
||||
import { Code, Eye, Pen, Plus, Share, Trash, Circle, FileUp } from 'lucide-svelte'
|
||||
import { goto } from '$lib/navigation'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
|
||||
@@ -36,6 +36,7 @@
|
||||
import SqsTriggerEditor from '$lib/components/triggers/sqs/SqsTriggerEditor.svelte'
|
||||
import { ALL_DEPLOYABLE, isDeployable } from '$lib/utils_deployable'
|
||||
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
|
||||
import { AwsIcon } from '$lib/components/icons'
|
||||
|
||||
type TriggerD = SqsTrigger & { canWrite: boolean }
|
||||
|
||||
@@ -250,7 +251,7 @@
|
||||
<div class="flex flex-row items-center gap-2 mt-6">
|
||||
<div class="text-sm shrink-0"> Filter by path of </div>
|
||||
<ToggleButtonGroup bind:selected={selectedFilterKind} let:item>
|
||||
<ToggleButton small value="trigger" label="SQS trigger" icon={Database} {item} />
|
||||
<ToggleButton small value="trigger" label="SQS trigger" icon={AwsIcon} {item} />
|
||||
<ToggleButton small value="script_flow" label="Script/Flow" icon={Code} {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
@@ -418,7 +419,7 @@
|
||||
displayName: canWrite ? 'Share' : 'See Permissions',
|
||||
icon: Share,
|
||||
action: () => {
|
||||
shareModal.openDrawer(path, 'websocket_trigger')
|
||||
shareModal.openDrawer(path, 'sqs_trigger')
|
||||
}
|
||||
}
|
||||
]}
|
||||
|
||||
Reference in New Issue
Block a user